wip: peers

This commit is contained in:
Constantin
2025-09-13 18:56:00 +03:00
parent 3e8a1bd530
commit 4f65f95ac9
6 changed files with 1634 additions and 100 deletions
+911 -43
View File
File diff suppressed because it is too large Load Diff
+378 -42
View File
@@ -1,11 +1,15 @@
import { useState, useEffect } from 'react';
import { Shield, Key, Users } from 'lucide-react';
import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle } from 'lucide-react';
import { wireguardAPI } from '../services/api';
function WireGuard() {
const [status, setStatus] = useState(null);
const [peers, setPeers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [selectedPeer, setSelectedPeer] = useState(null);
const [showPeerConfig, setShowPeerConfig] = useState(false);
const [peerConfig, setPeerConfig] = useState('');
useEffect(() => {
fetchWireGuardData();
@@ -19,14 +23,73 @@ function WireGuard() {
]);
setStatus(statusResponse.data);
setPeers(peersResponse.data);
setPeers(peersResponse.data || []);
} catch (error) {
console.error('Failed to fetch WireGuard data:', error);
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
const refreshData = async () => {
setIsRefreshing(true);
await fetchWireGuardData();
};
const handleViewPeerConfig = async (peer) => {
setSelectedPeer(peer);
try {
const response = await wireguardAPI.getPeerConfig({ name: peer.name });
setPeerConfig(response.data.config || 'Configuration not available');
} catch (error) {
console.error('Failed to get peer config:', error);
setPeerConfig('Failed to load configuration');
}
setShowPeerConfig(true);
};
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
alert('Configuration copied to clipboard!');
} catch (error) {
console.error('Failed to copy to clipboard:', error);
alert('Failed to copy to clipboard. Please copy manually.');
}
};
const downloadConfig = (peerName, config) => {
const blob = new Blob([config], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${peerName}.conf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getPeerStatus = (peer) => {
// This would need to be implemented based on actual peer status
// For now, we'll show a mock status
return {
online: Math.random() > 0.3, // Random for demo
lastHandshake: new Date(Date.now() - Math.random() * 3600000).toISOString(),
transferRx: Math.floor(Math.random() * 1000000),
transferTx: Math.floor(Math.random() * 1000000)
};
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
@@ -38,55 +101,328 @@ function WireGuard() {
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">WireGuard</h1>
<p className="mt-2 text-gray-600">
Manage WireGuard VPN configuration and peers
</p>
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">WireGuard VPN</h1>
<p className="mt-2 text-gray-600">
Manage WireGuard VPN configuration and monitor active peers
</p>
</div>
<button
onClick={refreshData}
disabled={isRefreshing}
className="btn btn-secondary flex items-center"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
{/* Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="card">
<div className="flex items-center mb-4">
<Shield className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Status</h3>
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg">
<Shield className="h-6 w-6 text-primary-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Service Status</p>
<p className={`text-lg font-semibold ${status?.running ? 'text-green-600' : 'text-red-600'}`}>
{status?.running ? 'Online' : 'Offline'}
</p>
</div>
</div>
{status ? (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Interface:</span>
<span className="text-sm font-medium">{status.interface || 'wg0'}</span>
</div>
<div className="card">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Users className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Peers</p>
<p className="text-lg font-semibold text-gray-900">{peers.length}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<Activity className="h-6 w-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Active Peers</p>
<p className="text-lg font-semibold text-gray-900">
{peers.filter(peer => getPeerStatus(peer).online).length}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<Wifi className="h-6 w-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Interface</p>
<p className="text-lg font-semibold text-gray-900">{status?.interface || 'wg0'}</p>
</div>
</div>
</div>
</div>
{/* Traffic Stats */}
{status?.total_traffic && (
<div className="card mb-8">
<div className="flex items-center mb-4">
<Activity className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Traffic Statistics</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p className="text-sm text-gray-500 mb-1">Data Received</p>
<p className="text-2xl font-bold text-green-600">
{formatBytes(status.total_traffic.bytes_received || 0)}
</p>
</div>
<div>
<p className="text-sm text-gray-500 mb-1">Data Sent</p>
<p className="text-2xl font-bold text-blue-600">
{formatBytes(status.total_traffic.bytes_sent || 0)}
</p>
</div>
</div>
</div>
)}
{/* Peers Table */}
<div className="card">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<Users className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Connected Peers</h3>
</div>
<div className="text-sm text-gray-500">
{peers.length} peer{peers.length !== 1 ? 's' : ''} configured
</div>
</div>
{peers.length === 0 ? (
<div className="text-center py-12">
<Shield className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No peers configured</h3>
<p className="text-gray-500 mb-4">Add your first peer to start using WireGuard VPN</p>
<button
onClick={() => window.location.href = '/peers'}
className="btn btn-primary"
>
Add Peer
</button>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Peer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
IP Address
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Handshake
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Traffic
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{peers.map((peer, index) => {
const peerStatus = getPeerStatus(peer);
return (
<tr key={index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-8 w-8">
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
<Shield className="h-4 w-4 text-primary-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{peer.name}</div>
{peer.description && (
<div className="text-sm text-gray-500">{peer.description}</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{peer.ip || peer.AllowedIPs || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
peerStatus.online
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${
peerStatus.online ? 'bg-green-400' : 'bg-red-400'
}`} />
{peerStatus.online ? 'Online' : 'Offline'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{peerStatus.online
? new Date(peerStatus.lastHandshake).toLocaleString()
: 'Never'
}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="space-y-1">
<div className="flex items-center text-xs">
<span className="text-green-600 mr-1"></span>
{formatBytes(peerStatus.transferRx)}
</div>
<div className="flex items-center text-xs">
<span className="text-blue-600 mr-1"></span>
{formatBytes(peerStatus.transferTx)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => handleViewPeerConfig(peer)}
className="text-primary-600 hover:text-primary-900"
title="View Configuration"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={() => downloadConfig(peer.name, peerConfig)}
className="text-primary-600 hover:text-primary-900"
title="Download Config"
>
<Download className="h-4 w-4" />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
{/* Peer Configuration Modal */}
{showPeerConfig && selectedPeer && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Shield className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">
{selectedPeer.name} Configuration
</h3>
</div>
<button
onClick={() => setShowPeerConfig(false)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Status:</span>
<span className="text-sm font-medium text-success-600">Active</span>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Peer Name
</label>
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.name}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
IP Address
</label>
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.ip || selectedPeer.AllowedIPs}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key
</label>
<p className="text-xs text-gray-900 bg-gray-50 p-2 rounded break-all">{selectedPeer.public_key || selectedPeer.PublicKey}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Allowed IPs
</label>
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.allowed_ips || selectedPeer.AllowedIPs}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
WireGuard Configuration
</label>
<div className="relative">
<textarea
value={peerConfig}
readOnly
className="w-full h-64 p-3 border border-gray-300 rounded-md font-mono text-sm bg-gray-50"
placeholder="Loading configuration..."
/>
<div className="absolute top-2 right-2 flex space-x-2">
<button
onClick={() => copyToClipboard(peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Copy to clipboard"
>
<Copy className="h-4 w-4 mr-1" />
Copy
</button>
<button
onClick={() => downloadConfig(selectedPeer.name, peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Download config file"
>
<Download className="h-4 w-4 mr-1" />
Download
</button>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Use this configuration on your mobile device or computer to connect to this WireGuard network.
</p>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={() => setShowPeerConfig(false)}
className="btn btn-secondary"
>
Close
</button>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">Status unavailable</p>
)}
</div>
{/* Peers */}
<div className="card">
<div className="flex items-center mb-4">
<Users className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Peers</h3>
</div>
<div className="space-y-2">
{peers.length > 0 ? (
peers.map((peer, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{peer.name}</span>
<span className="text-sm text-gray-500">{peer.ip}</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No peers configured</p>
)}
</div>
</div>
</div>
)}
</div>
);
}