diff --git a/src/components/dashboard/SipCredentialsCard.jsx b/src/components/dashboard/SipCredentialsCard.jsx index 34522b6..71b1f6a 100644 --- a/src/components/dashboard/SipCredentialsCard.jsx +++ b/src/components/dashboard/SipCredentialsCard.jsx @@ -5,7 +5,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; 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 { AlertDialog, AlertDialogAction, @@ -19,7 +19,7 @@ import { } from "@/components/ui/alert-dialog" import { Skeleton } from '@/components/ui/skeleton'; -function CredentialRow({ icon, label, value, onCopy }) { +function CredentialRow({ icon, label, value, onCopy, isCopied }) { return (
@@ -30,16 +30,17 @@ function CredentialRow({ icon, label, value, onCopy }) {
); } -export function SipCredentialsCard({ details, loading }) { +export function SipCredentialsCard({ details, loading, refetchDetails }) { const { token } = useAuth(); const [newSecret, setNewSecret] = useState(null); const [isResetting, setIsResetting] = useState(false); + const [copiedState, setCopiedState] = useState({}); const handleResetSecret = async () => { if (!token) return; @@ -51,6 +52,9 @@ export function SipCredentialsCard({ details, loading }) { }); const data = await res.json(); setNewSecret(data.newSecret); + if (refetchDetails) { + refetchDetails(); + } } catch (error) { console.error("Failed to reset secret", error); alert('Failed to reset secret.'); @@ -59,9 +63,14 @@ export function SipCredentialsCard({ details, loading }) { } }; - const copyToClipboard = (text) => { - navigator.clipboard.writeText(text); - alert('Copied to clipboard!'); + const copyToClipboard = (text, id) => { + if (!text) return; + navigator.clipboard.writeText(text).then(() => { + setCopiedState(prev => ({ ...prev, [id]: true })); + setTimeout(() => { + setCopiedState(prev => ({ ...prev, [id]: false })); + }, 2000); + }); }; if (loading) { @@ -72,6 +81,7 @@ export function SipCredentialsCard({ details, loading }) { + @@ -87,10 +97,11 @@ export function SipCredentialsCard({ details, loading }) { Use these details to connect your SIP client. - } label="SIP Server" value="pbx.litenet.tel" onCopy={() => copyToClipboard('pbx.litenet.tel')} /> - } label="SIP Username" value={details?.extensionId} onCopy={() => copyToClipboard(details?.extensionId)} /> + } label="SIP Server" value="pbx.litenet.tel" onCopy={() => copyToClipboard('pbx.litenet.tel', 'server')} isCopied={copiedState['server']} /> + } label="SIP Username" value={details?.extensionId} onCopy={() => copyToClipboard(details?.extensionId, 'username')} isCopied={copiedState['username']} /> + } label="SIP Secret" value="••••••••••••" onCopy={() => copyToClipboard(details?.user?.extPassword, 'secret')} isCopied={copiedState['secret']} /> - !open && setNewSecret(null)}> + { if (!open) { setNewSecret(null); setCopiedState(prev => ({ ...prev, newSecret: false })); } }}> diff --git a/src/components/dashboard/api-keys.jsx b/src/components/dashboard/api-keys.jsx index e23bbef..7e6f6c5 100644 --- a/src/components/dashboard/api-keys.jsx +++ b/src/components/dashboard/api-keys.jsx @@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com 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 { Trash2, PlusCircle, Copy, KeyRound, AlertTriangle, Check } 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'; @@ -34,6 +34,7 @@ export default function ApiKeys() { const [keyToDelete, setKeyToDelete] = useState(null); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreating, setIsCreating] = useState(false); + const [isCopied, setIsCopied] = useState(false); const fetchKeys = useCallback(async () => { if (!token) return; @@ -43,9 +44,10 @@ export default function ApiKeys() { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); - setKeys(data); + setKeys(Array.isArray(data) ? data : []); } catch (error) { console.error("Failed to fetch API keys", error); + setKeys([]); } finally { setLoading(false); } @@ -67,11 +69,26 @@ export default function ApiKeys() { }, body: JSON.stringify({ expiresInDays }) }); - const data = await res.json(); - setNewKey(data.key); - fetchKeys(); + const newKeyData = await res.json(); + if (!res.ok) { + 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) { console.error("Failed to create key", error); + alert(`Failed to create key: ${error.message}`); } finally { setIsCreating(false); } @@ -80,19 +97,26 @@ export default function ApiKeys() { const handleDeleteKey = async (keyToDelete) => { if (!token) return; 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', 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) { console.error("Failed to delete key", error); + alert(`Failed to delete key: ${error.message}`); } }; const copyToClipboard = (text) => { - navigator.clipboard.writeText(text); - alert('Key copied to clipboard!'); + navigator.clipboard.writeText(text).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }); }; const filteredKeys = keys @@ -100,36 +124,23 @@ export default function ApiKeys() { .filter(key => key.key.toLowerCase().includes(searchTerm.toLowerCase())); return ( - - -
-
- API Keys - Manage your API keys for programmatic access. -
- { - if (isCreating) return; - setIsCreateDialogOpen(open); - if (!open) setNewKey(null); - }}> - - - - - - {newKey ? 'API Key Created' : 'Create New API Key'} - - {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.'} - - - {newKey ? ( -
- {newKey} - -
- ) : ( + <> + + +
+
+ API Keys + Manage your API keys for programmatic access. +
+ + + + + + + Create New API Key + Set an expiration for your new key. +
@@ -139,87 +150,87 @@ export default function ApiKeys() { {isCreating ? 'Generating...' : 'Generate Key'}
- )} - -
-
-
- -
- setSearchTerm(e.target.value)} - className="max-w-xs bg-gray-900 border-gray-700" - /> -
- - - + +
- - {loading ? ( -
- - - + + +
+ setSearchTerm(e.target.value)} + className="max-w-xs bg-gray-900 border-gray-700" + /> +
+ + + +
- ) : filteredKeys.length > 0 ? ( - - - - Key - Type - IP Address - Created - Expires - Actions - - - - {filteredKeys.map(key => { - const isCurrentSession = key.type === 'session' && token?.startsWith(key.key); - return ( - - - {key.type === 'api' ? `${key.key.substring(0, 12)}...` : key.key} - {isCurrentSession && (this session)} - - - - {key.type} - - - {key.ipAddress || 'N/A'} - {format(new Date(key.createdAt), 'PP')} - {key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'} - - {!isCurrentSession && ( - - )} - - - ); - })} - -
- ) : ( -
- -

- {searchTerm || filterType !== 'all' ? 'No Matching Keys' : 'No API Keys'} -

-

- {searchTerm || filterType !== 'all' - ? 'Try adjusting your search or filter.' - : 'Create your first API key to get started with integrations.'} -

-
- )} -
- + {loading ? ( +
+ + + +
+ ) : filteredKeys.length > 0 ? ( + + + + Key + Type + IP Address + Created + Expires + Actions + + + + {filteredKeys.map(key => { + const isCurrentSession = key.type === 'session' && token?.startsWith(key.key); + return ( + + + {key.type === 'api' ? `${key.key.substring(0, 12)}...` : key.key} + {isCurrentSession && (this session)} + + + + {key.type} + + + {key.ipAddress || 'N/A'} + {format(new Date(key.createdAt), 'PP')} + {key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'} + + {!isCurrentSession && ( + + )} + + + ); + })} + +
+ ) : ( +
+ +

+ {searchTerm || filterType !== 'all' ? 'No Matching Keys' : 'No API Keys'} +

+

+ {searchTerm || filterType !== 'all' + ? 'Try adjusting your search or filter.' + : 'Create your first API key to get started with integrations.'} +

+
+ )} + + + !open && setKeyToDelete(null)}> Are you absolutely sure? @@ -247,6 +258,28 @@ export default function ApiKeys() { - + { if (!open) { setNewKey(null); setIsCopied(false); } }}> + + + API Key Created Successfully + + Your new API key is shown below. Copy it now, as you will not be able to see it again. + + +
+ {newKey} + +
+
+ +

+ Treat this key like a password. Do not share it with anyone or expose it in client-side code. +

+
+
+
+ ); } diff --git a/src/components/dashboard/dashboard-client.jsx b/src/components/dashboard/dashboard-client.jsx index c095e0c..9e58288 100644 --- a/src/components/dashboard/dashboard-client.jsx +++ b/src/components/dashboard/dashboard-client.jsx @@ -57,6 +57,7 @@ export default function DashboardClient() { }); if (res.ok) { const data = await res.json(); + console.log("Fetched details:", data); setDetails(data); } else { setDetails(null); diff --git a/src/components/dashboard/registered-devices.jsx b/src/components/dashboard/registered-devices.jsx index 7f78713..3739bbc 100644 --- a/src/components/dashboard/registered-devices.jsx +++ b/src/components/dashboard/registered-devices.jsx @@ -5,7 +5,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; 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'; function DeviceCard({ device }) { @@ -14,20 +14,49 @@ function DeviceCard({ device }) { if (ping > 150) pingColor = 'text-yellow-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 (
- {device.useragent} + {device.useragent || 'Unknown User Agent'}
-
-
- - {device.ip}:{device.port} +
+
+
+ + Client IP +
+ {device.ip}:{device.port}
-
- - {ping.toFixed(2)}ms +
+
+ + Contact IP +
+ {uriIp}:{uriPort} +
+
+
+ + Latency +
+ {ping.toFixed(2)}ms