show secret, update connected devices, and add checkmark anim for copy
All checks were successful
Deploy to Server / deploy (push) Successful in 1m52s
All checks were successful
Deploy to Server / deploy (push) Successful in 1m52s
This commit is contained in:
parent
1a3edce184
commit
e6a9bdcdc8
|
@ -5,7 +5,7 @@ import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Copy, KeyRound, Server, User } from 'lucide-react';
|
import { Copy, KeyRound, Server, User, Check } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
@ -19,7 +19,7 @@ import {
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
function CredentialRow({ icon, label, value, onCopy }) {
|
function CredentialRow({ icon, label, value, onCopy, isCopied }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between bg-gray-900 p-3 rounded-lg border border-gray-800">
|
<div className="flex items-center justify-between bg-gray-900 p-3 rounded-lg border border-gray-800">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
@ -30,16 +30,17 @@ function CredentialRow({ icon, label, value, onCopy }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" onClick={onCopy}>
|
<Button variant="ghost" size="icon" onClick={onCopy}>
|
||||||
<Copy className="h-4 w-4" />
|
{isCopied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SipCredentialsCard({ details, loading }) {
|
export function SipCredentialsCard({ details, loading, refetchDetails }) {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [newSecret, setNewSecret] = useState(null);
|
const [newSecret, setNewSecret] = useState(null);
|
||||||
const [isResetting, setIsResetting] = useState(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
const [copiedState, setCopiedState] = useState({});
|
||||||
|
|
||||||
const handleResetSecret = async () => {
|
const handleResetSecret = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
@ -51,6 +52,9 @@ export function SipCredentialsCard({ details, loading }) {
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setNewSecret(data.newSecret);
|
setNewSecret(data.newSecret);
|
||||||
|
if (refetchDetails) {
|
||||||
|
refetchDetails();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to reset secret", error);
|
console.error("Failed to reset secret", error);
|
||||||
alert('Failed to reset secret.');
|
alert('Failed to reset secret.');
|
||||||
|
@ -59,9 +63,14 @@ export function SipCredentialsCard({ details, loading }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = (text) => {
|
const copyToClipboard = (text, id) => {
|
||||||
navigator.clipboard.writeText(text);
|
if (!text) return;
|
||||||
alert('Copied to clipboard!');
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopiedState(prev => ({ ...prev, [id]: true }));
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopiedState(prev => ({ ...prev, [id]: false }));
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -72,6 +81,7 @@ export function SipCredentialsCard({ details, loading }) {
|
||||||
<Skeleton className="h-4 w-64 mt-2" />
|
<Skeleton className="h-4 w-64 mt-2" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-4">
|
<CardContent className="space-y-4 pt-4">
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
<Skeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-full" />
|
||||||
<Skeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-full" />
|
||||||
<Skeleton className="h-10 w-full" />
|
<Skeleton className="h-10 w-full" />
|
||||||
|
@ -87,10 +97,11 @@ export function SipCredentialsCard({ details, loading }) {
|
||||||
<CardDescription>Use these details to connect your SIP client.</CardDescription>
|
<CardDescription>Use these details to connect your SIP client.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<CredentialRow icon={<Server className="h-5 w-5 text-gray-400" />} label="SIP Server" value="pbx.litenet.tel" onCopy={() => copyToClipboard('pbx.litenet.tel')} />
|
<CredentialRow icon={<Server className="h-5 w-5 text-gray-400" />} label="SIP Server" value="pbx.litenet.tel" onCopy={() => copyToClipboard('pbx.litenet.tel', 'server')} isCopied={copiedState['server']} />
|
||||||
<CredentialRow icon={<User className="h-5 w-5 text-gray-400" />} label="SIP Username" value={details?.extensionId} onCopy={() => copyToClipboard(details?.extensionId)} />
|
<CredentialRow icon={<User className="h-5 w-5 text-gray-400" />} label="SIP Username" value={details?.extensionId} onCopy={() => copyToClipboard(details?.extensionId, 'username')} isCopied={copiedState['username']} />
|
||||||
|
<CredentialRow icon={<KeyRound className="h-5 w-5 text-gray-400" />} label="SIP Secret" value="••••••••••••" onCopy={() => copyToClipboard(details?.user?.extPassword, 'secret')} isCopied={copiedState['secret']} />
|
||||||
|
|
||||||
<Dialog open={!!newSecret} onOpenChange={(open) => !open && setNewSecret(null)}>
|
<Dialog open={!!newSecret} onOpenChange={(open) => { if (!open) { setNewSecret(null); setCopiedState(prev => ({ ...prev, newSecret: false })); } }}>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" disabled={isResetting} className="w-full">
|
<Button variant="destructive" disabled={isResetting} className="w-full">
|
||||||
|
@ -125,8 +136,8 @@ export function SipCredentialsCard({ details, loading }) {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex items-center space-x-2 bg-black p-3 rounded-md border border-gray-700">
|
<div className="flex items-center space-x-2 bg-black p-3 rounded-md border border-gray-700">
|
||||||
<code className="text-lg font-mono flex-grow text-green-400">{newSecret}</code>
|
<code className="text-lg font-mono flex-grow text-green-400">{newSecret}</code>
|
||||||
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(newSecret)}>
|
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(newSecret, 'newSecret')}>
|
||||||
<Copy className="h-4 w-4" />
|
{copiedState['newSecret'] ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Trash2, PlusCircle, Copy, KeyRound } from 'lucide-react';
|
import { Trash2, PlusCircle, Copy, KeyRound, AlertTriangle, Check } from 'lucide-react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
@ -34,6 +34,7 @@ export default function ApiKeys() {
|
||||||
const [keyToDelete, setKeyToDelete] = useState(null);
|
const [keyToDelete, setKeyToDelete] = useState(null);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
const fetchKeys = useCallback(async () => {
|
const fetchKeys = useCallback(async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
@ -43,9 +44,10 @@ export default function ApiKeys() {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setKeys(data);
|
setKeys(Array.isArray(data) ? data : []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch API keys", error);
|
console.error("Failed to fetch API keys", error);
|
||||||
|
setKeys([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -67,11 +69,26 @@ export default function ApiKeys() {
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ expiresInDays })
|
body: JSON.stringify({ expiresInDays })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const newKeyData = await res.json();
|
||||||
setNewKey(data.key);
|
if (!res.ok) {
|
||||||
fetchKeys();
|
throw new Error(newKeyData.error || 'Failed to create key.');
|
||||||
|
}
|
||||||
|
// Use newKeyData.apiKey from the response
|
||||||
|
setNewKey(newKeyData.apiKey);
|
||||||
|
|
||||||
|
// Add the new key to the local state, transforming the object to match the table's expected structure
|
||||||
|
const keyForState = {
|
||||||
|
...newKeyData,
|
||||||
|
key: newKeyData.apiKey, // Rename apiKey to key
|
||||||
|
};
|
||||||
|
delete keyForState.apiKey;
|
||||||
|
delete keyForState.message;
|
||||||
|
|
||||||
|
setKeys(prevKeys => [keyForState, ...prevKeys]);
|
||||||
|
setIsCreateDialogOpen(false); // Close the creation dialog
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create key", error);
|
console.error("Failed to create key", error);
|
||||||
|
alert(`Failed to create key: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
|
@ -80,19 +97,26 @@ export default function ApiKeys() {
|
||||||
const handleDeleteKey = async (keyToDelete) => {
|
const handleDeleteKey = async (keyToDelete) => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys/${keyToDelete}`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys/${keyToDelete}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
fetchKeys();
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json().catch(() => ({ error: 'Failed to delete key.' }));
|
||||||
|
throw new Error(errorData.error);
|
||||||
|
}
|
||||||
|
setKeys(prevKeys => prevKeys.filter(key => key.key !== keyToDelete));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete key", error);
|
console.error("Failed to delete key", error);
|
||||||
|
alert(`Failed to delete key: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = (text) => {
|
const copyToClipboard = (text) => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
alert('Key copied to clipboard!');
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredKeys = keys
|
const filteredKeys = keys
|
||||||
|
@ -100,36 +124,23 @@ export default function ApiKeys() {
|
||||||
.filter(key => key.key.toLowerCase().includes(searchTerm.toLowerCase()));
|
.filter(key => key.key.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mt-6 bg-gray-950/50 border-gray-800">
|
<>
|
||||||
<CardHeader>
|
<Card className="mt-6 bg-gray-950/50 border-gray-800">
|
||||||
<div className="flex justify-between items-center">
|
<CardHeader>
|
||||||
<div>
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>API Keys</CardTitle>
|
<div>
|
||||||
<CardDescription>Manage your API keys for programmatic access.</CardDescription>
|
<CardTitle>API Keys</CardTitle>
|
||||||
</div>
|
<CardDescription>Manage your API keys for programmatic access.</CardDescription>
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
|
</div>
|
||||||
if (isCreating) return;
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
setIsCreateDialogOpen(open);
|
<DialogTrigger asChild>
|
||||||
if (!open) setNewKey(null);
|
<Button><PlusCircle className="h-4 w-4 mr-2" /> Create New Key</Button>
|
||||||
}}>
|
</DialogTrigger>
|
||||||
<DialogTrigger asChild>
|
<DialogContent className="bg-gray-950 border-gray-800 text-white">
|
||||||
<Button><PlusCircle className="h-4 w-4 mr-2" /> Create New Key</Button>
|
<DialogHeader>
|
||||||
</DialogTrigger>
|
<DialogTitle>Create New API Key</DialogTitle>
|
||||||
<DialogContent>
|
<DialogDescription>Set an expiration for your new key.</DialogDescription>
|
||||||
<DialogHeader>
|
</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 className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="expiresIn">Expires in (days)</Label>
|
<Label htmlFor="expiresIn">Expires in (days)</Label>
|
||||||
|
@ -139,87 +150,87 @@ export default function ApiKeys() {
|
||||||
{isCreating ? 'Generating...' : 'Generate Key'}
|
{isCreating ? 'Generating...' : 'Generate Key'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</DialogContent>
|
||||||
</DialogContent>
|
</Dialog>
|
||||||
</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>
|
||||||
</div>
|
</CardHeader>
|
||||||
{loading ? (
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col sm:flex-row gap-4 justify-between mb-4">
|
||||||
<Skeleton className="h-10 w-full" />
|
<Input
|
||||||
<Skeleton className="h-10 w-full" />
|
placeholder="Search keys..."
|
||||||
<Skeleton className="h-10 w-full" />
|
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>
|
</div>
|
||||||
) : filteredKeys.length > 0 ? (
|
{loading ? (
|
||||||
<Table>
|
<div className="space-y-2">
|
||||||
<TableHeader>
|
<Skeleton className="h-10 w-full" />
|
||||||
<TableRow>
|
<Skeleton className="h-10 w-full" />
|
||||||
<TableHead>Key</TableHead>
|
<Skeleton className="h-10 w-full" />
|
||||||
<TableHead>Type</TableHead>
|
</div>
|
||||||
<TableHead>IP Address</TableHead>
|
) : filteredKeys.length > 0 ? (
|
||||||
<TableHead>Created</TableHead>
|
<Table>
|
||||||
<TableHead>Expires</TableHead>
|
<TableHeader>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableRow>
|
||||||
</TableRow>
|
<TableHead>Key</TableHead>
|
||||||
</TableHeader>
|
<TableHead>Type</TableHead>
|
||||||
<TableBody>
|
<TableHead>IP Address</TableHead>
|
||||||
{filteredKeys.map(key => {
|
<TableHead>Created</TableHead>
|
||||||
const isCurrentSession = key.type === 'session' && token?.startsWith(key.key);
|
<TableHead>Expires</TableHead>
|
||||||
return (
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
<TableRow key={key.key} className="hover:bg-gray-900">
|
</TableRow>
|
||||||
<TableCell className="font-mono">
|
</TableHeader>
|
||||||
{key.type === 'api' ? `${key.key.substring(0, 12)}...` : key.key}
|
<TableBody>
|
||||||
{isCurrentSession && <span className="text-xs text-gray-400 ml-2">(this session)</span>}
|
{filteredKeys.map(key => {
|
||||||
</TableCell>
|
const isCurrentSession = key.type === 'session' && token?.startsWith(key.key);
|
||||||
<TableCell>
|
return (
|
||||||
<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'}>
|
<TableRow key={key.key} className="hover:bg-gray-900">
|
||||||
{key.type}
|
<TableCell className="font-mono">
|
||||||
</Badge>
|
{key.type === 'api' ? `${key.key.substring(0, 12)}...` : key.key}
|
||||||
</TableCell>
|
{isCurrentSession && <span className="text-xs text-gray-400 ml-2">(this session)</span>}
|
||||||
<TableCell>{key.ipAddress || 'N/A'}</TableCell>
|
</TableCell>
|
||||||
<TableCell>{format(new Date(key.createdAt), 'PP')}</TableCell>
|
<TableCell>
|
||||||
<TableCell>{key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'}</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'}>
|
||||||
<TableCell className="text-right">
|
{key.type}
|
||||||
{!isCurrentSession && (
|
</Badge>
|
||||||
<Button variant="ghost" size="icon" onClick={() => setKeyToDelete(key)}>
|
</TableCell>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<TableCell>{key.ipAddress || 'N/A'}</TableCell>
|
||||||
</Button>
|
<TableCell>{format(new Date(key.createdAt), 'PP')}</TableCell>
|
||||||
)}
|
<TableCell>{key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'}</TableCell>
|
||||||
</TableCell>
|
<TableCell className="text-right">
|
||||||
</TableRow>
|
{!isCurrentSession && (
|
||||||
);
|
<Button variant="ghost" size="icon" onClick={() => setKeyToDelete(key)}>
|
||||||
})}
|
<Trash2 className="h-4 w-4 text-red-500" />
|
||||||
</TableBody>
|
</Button>
|
||||||
</Table>
|
)}
|
||||||
) : (
|
</TableCell>
|
||||||
<div className="text-center py-10 text-gray-400">
|
</TableRow>
|
||||||
<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'}
|
</TableBody>
|
||||||
</h3>
|
</Table>
|
||||||
<p>
|
) : (
|
||||||
{searchTerm || filterType !== 'all'
|
<div className="text-center py-10 text-gray-400">
|
||||||
? 'Try adjusting your search or filter.'
|
<KeyRound className="mx-auto h-12 w-12 mb-4" />
|
||||||
: 'Create your first API key to get started with integrations.'}
|
<h3 className="text-lg font-semibold text-white">
|
||||||
</p>
|
{searchTerm || filterType !== 'all' ? 'No Matching Keys' : 'No API Keys'}
|
||||||
</div>
|
</h3>
|
||||||
)}
|
<p>
|
||||||
</CardContent>
|
{searchTerm || filterType !== 'all'
|
||||||
<AlertDialog open={!!keyToDelete} onOpenChange={setKeyToDelete}>
|
? 'Try adjusting your search or filter.'
|
||||||
|
: 'Create your first API key to get started with integrations.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<AlertDialog open={!!keyToDelete} onOpenChange={(open) => !open && setKeyToDelete(null)}>
|
||||||
<AlertDialogContent className="bg-gray-950 border-gray-800 text-white">
|
<AlertDialogContent className="bg-gray-950 border-gray-800 text-white">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
@ -247,6 +258,28 @@ export default function ApiKeys() {
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</Card>
|
<Dialog open={!!newKey} onOpenChange={(open) => { if (!open) { setNewKey(null); setIsCopied(false); } }}>
|
||||||
|
<DialogContent className="bg-gray-950 border-gray-800 text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>API Key Created Successfully</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
Your new API key is shown below. Copy it now, as you will not be able to see it again.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex items-center space-x-2 bg-black p-3 rounded-md border border-gray-700">
|
||||||
|
<code className="text-sm font-mono flex-grow break-all">{newKey}</code>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(newKey)}>
|
||||||
|
{isCopied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3 bg-yellow-900/20 border border-yellow-700/50 text-yellow-300 p-3 rounded-md">
|
||||||
|
<AlertTriangle className="h-5 w-5 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm">
|
||||||
|
Treat this key like a password. Do not share it with anyone or expose it in client-side code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default function DashboardClient() {
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
console.log("Fetched details:", data);
|
||||||
setDetails(data);
|
setDetails(data);
|
||||||
} else {
|
} else {
|
||||||
setDetails(null);
|
setDetails(null);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { RefreshCw, Smartphone, Server, Wifi } from 'lucide-react';
|
import { RefreshCw, Smartphone, Globe, Network, Wifi } from 'lucide-react';
|
||||||
import { TbPlugConnectedX } from 'react-icons/tb';
|
import { TbPlugConnectedX } from 'react-icons/tb';
|
||||||
|
|
||||||
function DeviceCard({ device }) {
|
function DeviceCard({ device }) {
|
||||||
|
@ -14,20 +14,49 @@ function DeviceCard({ device }) {
|
||||||
if (ping > 150) pingColor = 'text-yellow-400';
|
if (ping > 150) pingColor = 'text-yellow-400';
|
||||||
if (ping > 300) pingColor = 'text-red-400';
|
if (ping > 300) pingColor = 'text-red-400';
|
||||||
|
|
||||||
|
// Extract IP and port from URI
|
||||||
|
const getIpFromUri = (uri) => {
|
||||||
|
try {
|
||||||
|
// Supports IPv4 and IPv6 addresses in brackets
|
||||||
|
const match = uri.match(/@((?:\[[a-fA-F0-9:]+\])|(?:[\d.]+)):(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
return { ip: match[1].replace(/[\[\]]/g, ''), port: match[2] };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return { ip: 'N/A', port: 'N/A' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { ip: uriIp, port: uriPort } = getIpFromUri(device.uri);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900/80 p-4 rounded-lg border border-gray-800 space-y-3">
|
<div className="bg-gray-900/80 p-4 rounded-lg border border-gray-800 space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Smartphone className="h-5 w-5 text-gray-400" />
|
<Smartphone className="h-5 w-5 text-gray-400" />
|
||||||
<span className="font-mono text-sm text-white truncate" title={device.useragent}>{device.useragent}</span>
|
<span className="font-mono text-sm text-white truncate" title={device.useragent}>{device.useragent || 'Unknown User Agent'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex items-center gap-2 text-gray-400">
|
<div className="flex justify-between items-center">
|
||||||
<Server className="h-4 w-4" />
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
<span>{device.ip}:{device.port}</span>
|
<Network className="h-4 w-4" />
|
||||||
|
<span>Client IP</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-white">{device.ip}:{device.port}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex items-center gap-2 font-semibold ${pingColor}`}>
|
<div className="flex justify-between items-center">
|
||||||
<Wifi className="h-4 w-4" />
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
<span>{ping.toFixed(2)}ms</span>
|
<Globe className="h-4 w-4" />
|
||||||
|
<span>Contact IP</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-white">{uriIp}:{uriPort}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
|
<Wifi className="h-4 w-4" />
|
||||||
|
<span>Latency</span>
|
||||||
|
</div>
|
||||||
|
<span className={`font-semibold ${pingColor}`}>{ping.toFixed(2)}ms</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue