This commit is contained in:
Constantin
2025-09-12 23:04:52 +03:00
commit 2277b11563
127 changed files with 23640 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useState, useEffect } from 'react';
import { Calendar as CalendarIcon, Users, Clock } from 'lucide-react';
import { calendarAPI } from '../services/api';
function Calendar() {
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchCalendarData();
}, []);
const fetchCalendarData = async () => {
try {
const [usersResponse, statusResponse] = await Promise.all([
calendarAPI.getUsers(),
calendarAPI.getStatus()
]);
setUsers(usersResponse.data);
setStatus(statusResponse.data);
} catch (error) {
console.error('Failed to fetch calendar data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Calendar Services</h1>
<p className="mt-2 text-gray-600">
Manage Radicale CalDAV and CardDAV services
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
<div className="card">
<div className="flex items-center mb-4">
<CalendarIcon className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Service Status</h3>
</div>
{status ? (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Radicale:</span>
<span className="text-sm font-medium text-success-600">Running</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">CalDAV:</span>
<span className="text-sm font-medium text-success-600">Active</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">CardDAV:</span>
<span className="text-sm font-medium text-success-600">Active</span>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">Status unavailable</p>
)}
</div>
{/* Users */}
<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">Calendar Users</h3>
</div>
<div className="space-y-2">
{users.length > 0 ? (
users.map((user, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{user.username}</span>
<span className="text-sm text-gray-500">{user.calendars || 0} calendars</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No calendar users configured</p>
)}
</div>
</div>
</div>
</div>
);
}
export default Calendar;
+284
View File
@@ -0,0 +1,284 @@
import { useState, useEffect } from 'react';
import {
Server,
Users,
Shield,
Mail,
Calendar,
FolderOpen,
Wifi,
Activity,
CheckCircle,
XCircle,
AlertCircle
} from 'lucide-react';
import { cellAPI, servicesAPI } from '../services/api';
function Dashboard({ isOnline }) {
const [cellStatus, setCellStatus] = useState(null);
const [servicesStatus, setServicesStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
if (!isOnline) {
setIsLoading(false);
return;
}
try {
const [statusResponse, servicesResponse] = await Promise.all([
cellAPI.getStatus(),
servicesAPI.getAllStatus()
]);
setCellStatus(statusResponse.data);
setServicesStatus(servicesResponse.data);
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, [isOnline]);
const getStatusIcon = (status) => {
if (status === true || status?.status === 'online' || status?.running === true) {
return <CheckCircle className="h-5 w-5 text-success-500" />;
} else if (status === false || status?.status === 'offline' || status?.running === false) {
return <XCircle className="h-5 w-5 text-danger-500" />;
} else {
return <AlertCircle className="h-5 w-5 text-warning-500" />;
}
};
const getStatusText = (status) => {
if (status === true || status?.status === 'online' || status?.running === true) {
return 'Online';
} else if (status === false || status?.status === 'offline' || status?.running === false) {
return 'Offline';
} else {
return 'Unknown';
}
};
const getStatusColor = (status) => {
if (status === true || status?.status === 'online' || status?.running === true) {
return 'text-success-600';
} else if (status === false || status?.status === 'offline' || status?.running === false) {
return 'text-danger-600';
} else {
return 'text-warning-600';
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="mt-2 text-gray-600">
Overview of your Personal Internet Cell status and services
</p>
</div>
{/* Cell Status */}
{cellStatus && (
<div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Cell Status</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card">
<div className="flex items-center">
<Server className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Cell Name</p>
<p className="text-lg font-semibold text-gray-900">{cellStatus.cell_name}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Users className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Peers</p>
<p className="text-lg font-semibold text-gray-900">{cellStatus.peers_count}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Activity className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Uptime</p>
<p className="text-lg font-semibold text-gray-900">
{Math.floor((cellStatus.uptime || 0) / 3600)}h {Math.floor(((cellStatus.uptime || 0) % 3600) / 60)}m
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
<div className="h-3 w-3 rounded-full bg-primary-600"></div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Status</p>
<p className="text-lg font-semibold text-gray-900">Active</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Services Status */}
{servicesStatus && (
<div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Services Status</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Shield className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">WireGuard</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.wireguard)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.wireguard)}`}>
{getStatusText(servicesStatus.wireguard)}
</span>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Mail className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Email</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.email)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.email)}`}>
{getStatusText(servicesStatus.email)}
</span>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Calendar className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Calendar</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.calendar)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.calendar)}`}>
{getStatusText(servicesStatus.calendar)}
</span>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<FolderOpen className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Files</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.files)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.files)}`}>
{getStatusText(servicesStatus.files)}
</span>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Wifi className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Routing</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.routing)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.routing)}`}>
{getStatusText(servicesStatus.routing)}
</span>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Server className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Network</span>
</div>
<div className="flex items-center">
{getStatusIcon(servicesStatus.network)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.network)}`}>
{getStatusText(servicesStatus.network)}
</span>
</div>
</div>
</div>
</div>
</div>
)}
{/* Quick Actions */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<button className="card hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center">
<Users className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Manage Peers</span>
</div>
</button>
<button className="card hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center">
<Shield className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">WireGuard Config</span>
</div>
</button>
<button className="card hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center">
<Wifi className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">Routing Rules</span>
</div>
</button>
<button className="card hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center">
<Activity className="h-6 w-6 text-primary-500" />
<span className="ml-3 text-sm font-medium text-gray-900">View Logs</span>
</div>
</button>
</div>
</div>
</div>
);
}
export default Dashboard;
+94
View File
@@ -0,0 +1,94 @@
import { useState, useEffect } from 'react';
import { Mail, Users, Send } from 'lucide-react';
import { emailAPI } from '../services/api';
function Email() {
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchEmailData();
}, []);
const fetchEmailData = async () => {
try {
const [usersResponse, statusResponse] = await Promise.all([
emailAPI.getUsers(),
emailAPI.getStatus()
]);
setUsers(usersResponse.data);
setStatus(statusResponse.data);
} catch (error) {
console.error('Failed to fetch email data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Email Services</h1>
<p className="mt-2 text-gray-600">
Manage Postfix and Dovecot email services
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
<div className="card">
<div className="flex items-center mb-4">
<Mail className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Service Status</h3>
</div>
{status ? (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Postfix:</span>
<span className="text-sm font-medium text-success-600">Running</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Dovecot:</span>
<span className="text-sm font-medium text-success-600">Running</span>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">Status unavailable</p>
)}
</div>
{/* Users */}
<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">Email Users</h3>
</div>
<div className="space-y-2">
{users.length > 0 ? (
users.map((user, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{user.username}</span>
<span className="text-sm text-gray-500">{user.domain}</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No email users configured</p>
)}
</div>
</div>
</div>
</div>
);
}
export default Email;
+94
View File
@@ -0,0 +1,94 @@
import { useState, useEffect } from 'react';
import { FolderOpen, Users, HardDrive } from 'lucide-react';
import { fileAPI } from '../services/api';
function Files() {
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchFilesData();
}, []);
const fetchFilesData = async () => {
try {
const [usersResponse, statusResponse] = await Promise.all([
fileAPI.getUsers(),
fileAPI.getStatus()
]);
setUsers(usersResponse.data);
setStatus(statusResponse.data);
} catch (error) {
console.error('Failed to fetch files data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">File Storage</h1>
<p className="mt-2 text-gray-600">
Manage WebDAV file storage services
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
<div className="card">
<div className="flex items-center mb-4">
<HardDrive className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Service Status</h3>
</div>
{status ? (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">WebDAV:</span>
<span className="text-sm font-medium text-success-600">Running</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Storage:</span>
<span className="text-sm font-medium text-success-600">Available</span>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">Status unavailable</p>
)}
</div>
{/* Users */}
<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">Storage Users</h3>
</div>
<div className="space-y-2">
{users.length > 0 ? (
users.map((user, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{user.username}</span>
<span className="text-sm text-gray-500">{user.storage_used || '0'} MB</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No storage users configured</p>
)}
</div>
</div>
</div>
</div>
);
}
export default Files;
+164
View File
@@ -0,0 +1,164 @@
import { useState, useEffect } from 'react';
import { Activity, Clock, FileText, AlertTriangle } from 'lucide-react';
import { monitoringAPI } from '../services/api';
function Logs() {
const [backendLog, setBackendLog] = useState('');
const [healthHistory, setHealthHistory] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [tab, setTab] = useState('logs');
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setIsLoading(true);
try {
const [logRes, healthRes] = await Promise.all([
monitoringAPI.getBackendLogs(100),
monitoringAPI.getHealthHistory(),
]);
setBackendLog(logRes.data.log || '');
setHealthHistory(healthRes.data || []);
} catch (error) {
console.error('Failed to fetch monitoring data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">System Monitoring</h1>
<p className="mt-2 text-gray-600">
View backend logs and health history
</p>
</div>
<div className="mb-4 flex gap-4">
<button
className={`px-4 py-2 rounded ${tab === 'logs' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-800'}`}
onClick={() => setTab('logs')}
>
<FileText className="inline-block mr-2" /> Backend Logs
</button>
<button
className={`px-4 py-2 rounded ${tab === 'health' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-800'}`}
onClick={() => setTab('health')}
>
<Clock className="inline-block mr-2" /> Health History
</button>
</div>
{tab === 'logs' && (
<div className="card">
<div className="flex items-center mb-4">
<FileText className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Backend Logs (last 100 lines)</h3>
</div>
<div className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm h-96 overflow-y-auto">
<pre>{backendLog || 'No logs available.'}</pre>
</div>
</div>
)}
{tab === 'health' && (
<div className="card">
<div className="flex items-center mb-4">
<Clock className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Health History (last 100 checks)</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="bg-gray-100">
<th className="px-2 py-1 text-left">Timestamp</th>
<th className="px-2 py-1 text-left">Network</th>
<th className="px-2 py-1 text-left">WireGuard</th>
<th className="px-2 py-1 text-left">Email</th>
<th className="px-2 py-1 text-left">Calendar</th>
<th className="px-2 py-1 text-left">Files</th>
<th className="px-2 py-1 text-left">Routing</th>
<th className="px-2 py-1 text-left">Vault</th>
<th className="px-2 py-1 text-left">Alerts</th>
</tr>
</thead>
<tbody>
{healthHistory.map((h, i) => (
<tr key={i} className={h.alerts && h.alerts.length > 0 ? 'bg-red-100' : ''}>
<td className="px-2 py-1 font-mono">{h.timestamp}</td>
<td className="px-2 py-1">
{h.network?.status === 'online' || h.network?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.wireguard?.status === 'online' || h.wireguard?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.email?.status === 'online' || h.email?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.calendar?.status === 'online' || h.calendar?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.files?.status === 'online' || h.files?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.routing?.status === 'online' || h.routing?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.vault?.status === 'online' || h.vault?.running === true ?
<span className="text-green-600">OK</span> :
<span className="text-red-600 font-bold">Down</span>
}
</td>
<td className="px-2 py-1">
{h.alerts && h.alerts.length > 0 ? (
<div className="flex flex-col gap-1">
{h.alerts.map((a, j) => (
<span key={j} className="text-red-700 font-semibold flex items-center"><AlertTriangle className="inline-block h-4 w-4 mr-1 text-red-500" />{a}</span>
))}
</div>
) : (
<span className="text-green-600">None</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
export default Logs;
+117
View File
@@ -0,0 +1,117 @@
import { useState, useEffect } from 'react';
import { Network, Server, Clock } from 'lucide-react';
import { networkAPI } from '../services/api';
function NetworkServices() {
const [dnsRecords, setDnsRecords] = useState([]);
const [dhcpLeases, setDhcpLeases] = useState([]);
const [ntpStatus, setNtpStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchNetworkData();
}, []);
const fetchNetworkData = async () => {
try {
const [dnsResponse, dhcpResponse, ntpResponse] = await Promise.all([
networkAPI.getDNSRecords(),
networkAPI.getDHCPLeases(),
networkAPI.getNTPStatus()
]);
setDnsRecords(dnsResponse.data);
setDhcpLeases(dhcpResponse.data);
setNtpStatus(ntpResponse.data);
} catch (error) {
console.error('Failed to fetch network data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Network Services</h1>
<p className="mt-2 text-gray-600">
Manage DNS, DHCP, and NTP services
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* DNS Records */}
<div className="card">
<div className="flex items-center mb-4">
<Network className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">DNS Records</h3>
</div>
<div className="space-y-2">
{dnsRecords.length > 0 ? (
dnsRecords.map((record, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{record.name}</span>
<span className="text-sm text-gray-500">{record.ip}</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No DNS records configured</p>
)}
</div>
</div>
{/* DHCP Leases */}
<div className="card">
<div className="flex items-center mb-4">
<Server className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">DHCP Leases</h3>
</div>
<div className="space-y-2">
{dhcpLeases.length > 0 ? (
dhcpLeases.map((lease, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">{lease.hostname || 'Unknown'}</span>
<span className="text-sm text-gray-500">{lease.ip}</span>
</div>
))
) : (
<p className="text-gray-500 text-sm">No active DHCP leases</p>
)}
</div>
</div>
{/* NTP Status */}
<div className="card">
<div className="flex items-center mb-4">
<Clock className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">NTP Status</h3>
</div>
{ntpStatus ? (
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Status:</span>
<span className="text-sm font-medium text-success-600">Online</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Sync:</span>
<span className="text-sm font-medium text-success-600">Synchronized</span>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">NTP service unavailable</p>
)}
</div>
</div>
</div>
);
}
export default NetworkServices;
+269
View File
@@ -0,0 +1,269 @@
import { useState, useEffect } from 'react';
import { Plus, Trash2, Edit, Eye, Wifi, Shield } from 'lucide-react';
import { peerAPI } from '../services/api';
function Peers() {
const [peers, setPeers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [newPeer, setNewPeer] = useState({
name: '',
ip: '',
public_key: '',
allowed_ips: '',
description: ''
});
useEffect(() => {
fetchPeers();
}, []);
const fetchPeers = async () => {
try {
const response = await peerAPI.getPeers();
setPeers(response.data);
} catch (error) {
console.error('Failed to fetch peers:', error);
} finally {
setIsLoading(false);
}
};
const handleAddPeer = async (e) => {
e.preventDefault();
try {
await peerAPI.addPeer(newPeer);
setShowAddModal(false);
setNewPeer({ name: '', ip: '', public_key: '', allowed_ips: '', description: '' });
fetchPeers();
} catch (error) {
console.error('Failed to add peer:', error);
}
};
const handleRemovePeer = async (peerName) => {
if (window.confirm(`Are you sure you want to remove peer "${peerName}"?`)) {
try {
await peerAPI.removePeer(peerName);
fetchPeers();
} catch (error) {
console.error('Failed to remove peer:', error);
}
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Peers</h1>
<p className="mt-2 text-gray-600">
Manage peer connections and WireGuard configurations
</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="btn btn-primary flex items-center"
>
<Plus className="h-4 w-4 mr-2" />
Add Peer
</button>
</div>
</div>
{/* Peers List */}
<div className="card">
<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">
Name
</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">
Type
</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.length === 0 ? (
<tr>
<td colSpan="5" className="px-6 py-4 text-center text-gray-500">
No peers configured. Add your first peer to get started.
</td>
</tr>
) : (
peers.map((peer) => (
<tr key={peer.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<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>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{peer.ip}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="status-indicator status-online">
Online
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Shield className="h-4 w-4 text-primary-500 mr-2" />
<span className="text-sm text-gray-900">WireGuard</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
className="text-primary-600 hover:text-primary-900"
title="View Details"
>
<Eye className="h-4 w-4" />
</button>
<button
className="text-primary-600 hover:text-primary-900"
title="Edit Peer"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleRemovePeer(peer.name)}
className="text-danger-600 hover:text-danger-900"
title="Remove Peer"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Add Peer Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Add New Peer</h3>
<form onSubmit={handleAddPeer}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Peer Name
</label>
<input
type="text"
value={newPeer.name}
onChange={(e) => setNewPeer({ ...newPeer, name: e.target.value })}
className="input"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
IP Address
</label>
<input
type="text"
value={newPeer.ip}
onChange={(e) => setNewPeer({ ...newPeer, ip: e.target.value })}
className="input"
placeholder="10.0.0.1"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key
</label>
<textarea
value={newPeer.public_key}
onChange={(e) => setNewPeer({ ...newPeer, public_key: e.target.value })}
className="input"
rows="3"
placeholder="Enter WireGuard public key"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Allowed IPs
</label>
<input
type="text"
value={newPeer.allowed_ips}
onChange={(e) => setNewPeer({ ...newPeer, allowed_ips: e.target.value })}
className="input"
placeholder="192.168.1.0/24"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<input
type="text"
value={newPeer.description}
onChange={(e) => setNewPeer({ ...newPeer, description: e.target.value })}
className="input"
placeholder="Optional description"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Add Peer
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
}
export default Peers;
+707
View File
@@ -0,0 +1,707 @@
import { useState, useEffect } from 'react';
import { Plus, Trash2, Wifi, Shield, Activity, Settings } from 'lucide-react';
import { routingAPI } from '../services/api';
function Routing() {
const [routingStatus, setRoutingStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
// NAT management state
const [natRules, setNatRules] = useState([]);
const [natLoading, setNatLoading] = useState(false);
const [natError, setNatError] = useState(null);
const [showNatForm, setShowNatForm] = useState(false);
const [newNat, setNewNat] = useState({ source_network: '', target_interface: '', masquerade: true, nat_type: 'MASQUERADE', protocol: 'ALL', external_port: '', internal_ip: '', internal_port: '' });
const [natSubmitting, setNatSubmitting] = useState(false);
// Peer Routes management state
const [peerRoutes, setPeerRoutes] = useState([]);
const [peersLoading, setPeersLoading] = useState(false);
const [peersError, setPeersError] = useState(null);
const [showPeerForm, setShowPeerForm] = useState(false);
const [newPeerRoute, setNewPeerRoute] = useState({ peer_name: '', peer_ip: '', allowed_networks: '', route_type: 'lan' });
const [peerSubmitting, setPeerSubmitting] = useState(false);
// Firewall Rules management state
const [firewallRules, setFirewallRules] = useState([]);
const [fwLoading, setFwLoading] = useState(false);
const [fwError, setFwError] = useState(null);
const [showFwForm, setShowFwForm] = useState(false);
const [newFwRule, setNewFwRule] = useState({ rule_type: 'INPUT', source: '', destination: '', action: 'ACCEPT', protocol: 'ALL', port_range: '' });
const [fwSubmitting, setFwSubmitting] = useState(false);
useEffect(() => {
fetchRoutingStatus();
fetchNatRules();
fetchPeerRoutes();
fetchFirewallRules();
}, []);
const fetchRoutingStatus = async () => {
try {
const response = await routingAPI.getStatus();
setRoutingStatus(response.data);
} catch (error) {
console.error('Failed to fetch routing status:', error);
} finally {
setIsLoading(false);
}
};
const fetchNatRules = async () => {
setNatLoading(true);
setNatError(null);
try {
const response = await routingAPI.getNatRules();
setNatRules(response.data.nat_rules || []);
} catch (error) {
setNatError('Failed to load NAT rules');
} finally {
setNatLoading(false);
}
};
const fetchPeerRoutes = async () => {
setPeersLoading(true);
setPeersError(null);
try {
const response = await routingAPI.getPeerRoutes();
setPeerRoutes(response.data.peer_routes || []);
} catch (error) {
setPeersError('Failed to load peer routes');
} finally {
setPeersLoading(false);
}
};
const fetchFirewallRules = async () => {
setFwLoading(true);
setFwError(null);
try {
const response = await routingAPI.getFirewallRules();
setFirewallRules(response.data.firewall_rules || []);
} catch (error) {
setFwError('Failed to load firewall rules');
} finally {
setFwLoading(false);
}
};
const handleNatInputChange = (e) => {
const { name, value, type, checked } = e.target;
setNewNat((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleAddNatRule = async (e) => {
e.preventDefault();
setNatSubmitting(true);
setNatError(null);
try {
await routingAPI.addNatRule(newNat);
setShowNatForm(false);
setNewNat({ source_network: '', target_interface: '', masquerade: true, nat_type: 'MASQUERADE', protocol: 'ALL', external_port: '', internal_ip: '', internal_port: '' });
fetchNatRules();
fetchRoutingStatus();
} catch (error) {
setNatError('Failed to add NAT rule');
} finally {
setNatSubmitting(false);
}
};
const handleDeleteNatRule = async (ruleId) => {
if (!window.confirm('Delete this NAT rule?')) return;
setNatLoading(true);
setNatError(null);
try {
await routingAPI.deleteNatRule(ruleId);
fetchNatRules();
fetchRoutingStatus();
} catch (error) {
setNatError('Failed to delete NAT rule');
} finally {
setNatLoading(false);
}
};
const handlePeerInputChange = (e) => {
const { name, value } = e.target;
setNewPeerRoute((prev) => ({ ...prev, [name]: value }));
};
const handleAddPeerRoute = async (e) => {
e.preventDefault();
setPeerSubmitting(true);
setPeersError(null);
try {
// allowed_networks: comma-separated string to array
const payload = {
...newPeerRoute,
allowed_networks: newPeerRoute.allowed_networks.split(',').map((s) => s.trim()).filter(Boolean),
};
await routingAPI.addPeerRoute(payload);
setShowPeerForm(false);
setNewPeerRoute({ peer_name: '', peer_ip: '', allowed_networks: '', route_type: 'lan' });
fetchPeerRoutes();
fetchRoutingStatus();
} catch (error) {
setPeersError('Failed to add peer route');
} finally {
setPeerSubmitting(false);
}
};
const handleDeletePeerRoute = async (peerName) => {
if (!window.confirm('Delete this peer route?')) return;
setPeersLoading(true);
setPeersError(null);
try {
await routingAPI.deletePeerRoute(peerName);
fetchPeerRoutes();
fetchRoutingStatus();
} catch (error) {
setPeersError('Failed to delete peer route');
} finally {
setPeersLoading(false);
}
};
const handleFwInputChange = (e) => {
const { name, value } = e.target;
setNewFwRule((prev) => ({ ...prev, [name]: value }));
};
const handleAddFwRule = async (e) => {
e.preventDefault();
setFwSubmitting(true);
setFwError(null);
try {
const payload = { ...newFwRule };
if (!payload.port) delete payload.port;
await routingAPI.addFirewallRule(payload);
setShowFwForm(false);
setNewFwRule({ rule_type: 'INPUT', source: '', destination: '', action: 'ACCEPT', protocol: 'ALL', port_range: '' });
fetchFirewallRules();
fetchRoutingStatus();
} catch (error) {
setFwError('Failed to add firewall rule');
} finally {
setFwSubmitting(false);
}
};
const handleDeleteFwRule = async (ruleId) => {
if (!window.confirm('Delete this firewall rule?')) return;
setFwLoading(true);
setFwError(null);
try {
await routingAPI.deleteFirewallRule(ruleId);
fetchFirewallRules();
fetchRoutingStatus();
} catch (error) {
setFwError('Failed to delete firewall rule');
} finally {
setFwLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
const tabs = [
{ id: 'overview', name: 'Overview', icon: Activity },
{ id: 'nat', name: 'NAT Rules', icon: Shield },
{ id: 'peers', name: 'Peer Routes', icon: Wifi },
{ id: 'firewall', name: 'Firewall', icon: Settings },
];
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Routing & Gateway</h1>
<p className="mt-2 text-gray-600">
Manage VPN gateway, NAT rules, and routing configuration
</p>
</div>
{/* Status Overview */}
{routingStatus && (
<div className="mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card">
<div className="flex items-center">
<Shield className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">NAT Rules</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.nat_rules_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Wifi className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Peer Routes</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.peer_routes_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Settings className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Firewall Rules</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.firewall_rules_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Activity className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Exit Nodes</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.exit_nodes_count || 0}
</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Tabs */}
<div className="mb-6">
<nav className="flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center px-1 py-2 border-b-2 font-medium text-sm ${
activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<tab.icon className="h-4 w-4 mr-2" />
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="card">
{activeTab === 'overview' && (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Routing Overview</h3>
{routingStatus?.routing_table && routingStatus.routing_table.length > 0 ? (
<div className="space-y-2">
{routingStatus.routing_table.map((route, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center">
<Wifi className="h-4 w-4 text-primary-500 mr-2" />
<span className="text-sm font-medium text-gray-900">
{route.route}
</span>
</div>
<div className="text-xs text-gray-500">
{route.parsed?.via && `via ${route.parsed.via}`}
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500">No routing table entries available.</p>
)}
</div>
)}
{activeTab === 'nat' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">NAT Rules</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowNatForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showNatForm ? 'Cancel' : 'Add NAT Rule'}
</button>
</div>
{showNatForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddNatRule}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<input
type="text"
name="source_network"
placeholder="Source Network (e.g. 192.168.1.0/24)"
className="input"
value={newNat.source_network}
onChange={handleNatInputChange}
required
/>
<input
type="text"
name="target_interface"
placeholder="Target Interface (e.g. eth0)"
className="input"
value={newNat.target_interface}
onChange={handleNatInputChange}
required
/>
<label className="flex items-center space-x-2">
<input
type="checkbox"
name="masquerade"
checked={newNat.masquerade}
onChange={handleNatInputChange}
/>
<span>Masquerade</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<select
name="nat_type"
className="input"
value={newNat.nat_type}
onChange={handleNatInputChange}
>
<option value="MASQUERADE">MASQUERADE</option>
<option value="SNAT">SNAT</option>
<option value="DNAT">DNAT (Port Forward)</option>
</select>
<select
name="protocol"
className="input"
value={newNat.protocol}
onChange={handleNatInputChange}
>
<option value="ALL">ALL</option>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
</select>
<input
type="text"
name="external_port"
placeholder="External Port (for DNAT)"
className="input"
value={newNat.external_port}
onChange={handleNatInputChange}
/>
<input
type="text"
name="internal_ip"
placeholder="Internal IP (for DNAT)"
className="input"
value={newNat.internal_ip}
onChange={handleNatInputChange}
/>
<input
type="text"
name="internal_port"
placeholder="Internal Port (for DNAT)"
className="input"
value={newNat.internal_port}
onChange={handleNatInputChange}
/>
</div>
<div className="text-xs text-gray-500 mt-2">
<span>Advanced: Use DNAT for port forwarding, specify protocol/ports as needed.</span>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={natSubmitting}>
{natSubmitting ? 'Adding...' : 'Add Rule'}
</button>
</div>
{natError && <p className="text-red-500 mt-2">{natError}</p>}
</form>
)}
{natLoading ? (
<div className="py-8 text-center text-gray-500">Loading NAT rules...</div>
) : natError ? (
<div className="py-8 text-center text-red-500">{natError}</div>
) : natRules.length === 0 ? (
<div className="py-8 text-center text-gray-500">No NAT rules configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Source Network</th>
<th className="px-4 py-2 text-left">Target Interface</th>
<th className="px-4 py-2 text-left">Masquerade</th>
<th className="px-4 py-2 text-left">Type</th>
<th className="px-4 py-2 text-left">Protocol</th>
<th className="px-4 py-2 text-left">Ext Port</th>
<th className="px-4 py-2 text-left">Int IP</th>
<th className="px-4 py-2 text-left">Int Port</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{natRules.map((rule, idx) => (
<tr key={rule.id || idx} className="border-t">
<td className="px-4 py-2">{rule.source_network}</td>
<td className="px-4 py-2">{rule.target_interface}</td>
<td className="px-4 py-2">{rule.masquerade ? 'Yes' : 'No'}</td>
<td className="px-4 py-2">{rule.nat_type || 'MASQUERADE'}</td>
<td className="px-4 py-2">{rule.protocol || 'ALL'}</td>
<td className="px-4 py-2">{rule.external_port || '-'}</td>
<td className="px-4 py-2">{rule.internal_ip || '-'}</td>
<td className="px-4 py-2">{rule.internal_port || '-'}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeleteNatRule(rule.id || idx)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{activeTab === 'peers' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Peer Routes</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowPeerForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showPeerForm ? 'Cancel' : 'Add Peer Route'}
</button>
</div>
{showPeerForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddPeerRoute}>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
type="text"
name="peer_name"
placeholder="Peer Name"
className="input"
value={newPeerRoute.peer_name}
onChange={handlePeerInputChange}
required
/>
<input
type="text"
name="peer_ip"
placeholder="Peer IP (e.g. 10.0.0.2)"
className="input"
value={newPeerRoute.peer_ip}
onChange={handlePeerInputChange}
required
/>
<input
type="text"
name="allowed_networks"
placeholder="Allowed Networks (comma-separated)"
className="input"
value={newPeerRoute.allowed_networks}
onChange={handlePeerInputChange}
/>
<select
name="route_type"
className="input"
value={newPeerRoute.route_type}
onChange={handlePeerInputChange}
>
<option value="lan">LAN</option>
<option value="exit">Exit</option>
<option value="bridge">Bridge</option>
<option value="split">Split</option>
</select>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={peerSubmitting}>
{peerSubmitting ? 'Adding...' : 'Add Route'}
</button>
</div>
{peersError && <p className="text-red-500 mt-2">{peersError}</p>}
</form>
)}
{peersLoading ? (
<div className="py-8 text-center text-gray-500">Loading peer routes...</div>
) : peersError ? (
<div className="py-8 text-center text-red-500">{peersError}</div>
) : peerRoutes.length === 0 ? (
<div className="py-8 text-center text-gray-500">No peer routes configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Peer Name</th>
<th className="px-4 py-2 text-left">Peer IP</th>
<th className="px-4 py-2 text-left">Allowed Networks</th>
<th className="px-4 py-2 text-left">Route Type</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{peerRoutes.map((route, idx) => (
<tr key={route.peer_name || idx} className="border-t">
<td className="px-4 py-2">{route.peer_name}</td>
<td className="px-4 py-2">{route.peer_ip}</td>
<td className="px-4 py-2">{Array.isArray(route.allowed_networks) ? route.allowed_networks.join(', ') : route.allowed_networks}</td>
<td className="px-4 py-2">{route.route_type}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeletePeerRoute(route.peer_name)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{activeTab === 'firewall' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Firewall Rules</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowFwForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showFwForm ? 'Cancel' : 'Add Firewall Rule'}
</button>
</div>
{showFwForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddFwRule}>
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<select
name="rule_type"
className="input"
value={newFwRule.rule_type}
onChange={handleFwInputChange}
>
<option value="INPUT">INPUT</option>
<option value="OUTPUT">OUTPUT</option>
<option value="FORWARD">FORWARD</option>
</select>
<input
type="text"
name="source"
placeholder="Source (e.g. 192.168.1.0/24)"
className="input"
value={newFwRule.source}
onChange={handleFwInputChange}
required
/>
<input
type="text"
name="destination"
placeholder="Destination (e.g. 0.0.0.0/0)"
className="input"
value={newFwRule.destination}
onChange={handleFwInputChange}
required
/>
<select
name="protocol"
className="input"
value={newFwRule.protocol}
onChange={handleFwInputChange}
>
<option value="ALL">ALL</option>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
<option value="ICMP">ICMP</option>
</select>
<input
type="text"
name="port_range"
placeholder="Port or Range (e.g. 80 or 1000-2000)"
className="input"
value={newFwRule.port_range}
onChange={handleFwInputChange}
/>
<select
name="action"
className="input"
value={newFwRule.action}
onChange={handleFwInputChange}
>
<option value="ACCEPT">ACCEPT</option>
<option value="DROP">DROP</option>
<option value="REJECT">REJECT</option>
</select>
</div>
<div className="text-xs text-gray-500 mt-2">
<span>Advanced: Specify protocol and port/range for granular matching.</span>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={fwSubmitting}>
{fwSubmitting ? 'Adding...' : 'Add Rule'}
</button>
</div>
{fwError && <p className="text-red-500 mt-2">{fwError}</p>}
</form>
)}
{fwLoading ? (
<div className="py-8 text-center text-gray-500">Loading firewall rules...</div>
) : fwError ? (
<div className="py-8 text-center text-red-500">{fwError}</div>
) : firewallRules.length === 0 ? (
<div className="py-8 text-center text-gray-500">No firewall rules configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Rule Type</th>
<th className="px-4 py-2 text-left">Source</th>
<th className="px-4 py-2 text-left">Destination</th>
<th className="px-4 py-2 text-left">Protocol</th>
<th className="px-4 py-2 text-left">Port/Range</th>
<th className="px-4 py-2 text-left">Action</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{firewallRules.map((rule, idx) => (
<tr key={rule.id || idx} className="border-t">
<td className="px-4 py-2">{rule.rule_type}</td>
<td className="px-4 py-2">{rule.source}</td>
<td className="px-4 py-2">{rule.destination}</td>
<td className="px-4 py-2">{rule.protocol || 'ALL'}</td>
<td className="px-4 py-2">{rule.port_range || '-'}</td>
<td className="px-4 py-2">{rule.action}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeleteFwRule(rule.id || idx)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
</div>
);
}
export default Routing;
+98
View File
@@ -0,0 +1,98 @@
import { useState, useEffect } from 'react';
import { Settings as SettingsIcon, Server, Shield } from 'lucide-react';
import { cellAPI } from '../services/api';
function Settings() {
const [config, setConfig] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
try {
const response = await cellAPI.getConfig();
setConfig(response.data);
} catch (error) {
console.error('Failed to fetch config:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<p className="mt-2 text-gray-600">
Configure your Personal Internet Cell
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Cell Configuration */}
<div className="card">
<div className="flex items-center mb-4">
<Server className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Cell Configuration</h3>
</div>
{config ? (
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Cell Name:</span>
<span className="text-sm font-medium">{config.cell_name}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Domain:</span>
<span className="text-sm font-medium">{config.domain}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">IP Range:</span>
<span className="text-sm font-medium">{config.ip_range}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">WireGuard Port:</span>
<span className="text-sm font-medium">{config.wireguard_port}</span>
</div>
</div>
) : (
<p className="text-gray-500 text-sm">Configuration unavailable</p>
)}
</div>
{/* Security Settings */}
<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">Security Settings</h3>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">TLS Certificate:</span>
<span className="text-sm font-medium text-success-600">Valid</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Firewall:</span>
<span className="text-sm font-medium text-success-600">Active</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">VPN Encryption:</span>
<span className="text-sm font-medium text-success-600">Enabled</span>
</div>
</div>
</div>
</div>
</div>
);
}
export default Settings;
+451
View File
@@ -0,0 +1,451 @@
import { useState, useEffect } from 'react';
import { Shield, Key, Users, Plus, Trash2, Download } from 'lucide-react';
import { vaultAPI } from '../services/api';
function Vault() {
const [status, setStatus] = useState(null);
const [certificates, setCertificates] = useState([]);
const [trustedKeys, setTrustedKeys] = useState({});
const [trustChains, setTrustChains] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [showAddCertModal, setShowAddCertModal] = useState(false);
const [showAddKeyModal, setShowAddKeyModal] = useState(false);
const [newCert, setNewCert] = useState({
common_name: '',
domains: '',
key_size: 2048,
days: 365
});
const [newKey, setNewKey] = useState({
name: '',
public_key: '',
trust_level: 'direct'
});
useEffect(() => {
fetchVaultData();
}, []);
const fetchVaultData = async () => {
try {
const [statusResponse, certsResponse, keysResponse, chainsResponse] = await Promise.all([
vaultAPI.getStatus(),
vaultAPI.getCertificates(),
vaultAPI.getTrustedKeys(),
vaultAPI.getTrustChains()
]);
setStatus(statusResponse.data);
setCertificates(certsResponse.data);
setTrustedKeys(keysResponse.data);
setTrustChains(chainsResponse.data);
} catch (error) {
console.error('Failed to fetch vault data:', error);
} finally {
setIsLoading(false);
}
};
const handleGenerateCertificate = async (e) => {
e.preventDefault();
try {
const certData = {
...newCert,
domains: newCert.domains ? newCert.domains.split(',').map(d => d.trim()) : []
};
await vaultAPI.generateCertificate(certData);
setShowAddCertModal(false);
setNewCert({ common_name: '', domains: '', key_size: 2048, days: 365 });
fetchVaultData();
} catch (error) {
console.error('Failed to generate certificate:', error);
}
};
const handleAddTrustedKey = async (e) => {
e.preventDefault();
try {
await vaultAPI.addTrustedKey(newKey);
setShowAddKeyModal(false);
setNewKey({ name: '', public_key: '', trust_level: 'direct' });
fetchVaultData();
} catch (error) {
console.error('Failed to add trusted key:', error);
}
};
const handleRevokeCertificate = async (commonName) => {
if (window.confirm(`Are you sure you want to revoke certificate "${commonName}"?`)) {
try {
await vaultAPI.revokeCertificate(commonName);
fetchVaultData();
} catch (error) {
console.error('Failed to revoke certificate:', error);
}
}
};
const handleRemoveTrustedKey = async (name) => {
if (window.confirm(`Are you sure you want to remove trusted key "${name}"?`)) {
try {
await vaultAPI.removeTrustedKey(name);
fetchVaultData();
} catch (error) {
console.error('Failed to remove trusted key:', error);
}
}
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString();
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Vault & Trust</h1>
<p className="mt-2 text-gray-600">
Manage certificates, trust systems, and security settings
</p>
</div>
{/* Status Overview */}
{status && (
<div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Vault Status</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card">
<div className="flex items-center">
<Key className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Certificates</p>
<p className="text-lg font-semibold text-gray-900">{status.certificates_count}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Key className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Trusted Keys</p>
<p className="text-lg font-semibold text-gray-900">{status.trusted_keys_count}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Shield className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Trust Chains</p>
<p className="text-lg font-semibold text-gray-900">{status.trust_chains_count}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
<div className="h-3 w-3 rounded-full bg-primary-600"></div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">CA Status</p>
<p className="text-lg font-semibold text-gray-900">
{status.ca_configured ? 'Active' : 'Inactive'}
</p>
</div>
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Certificates */}
<div className="card">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<Key className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Certificates</h3>
</div>
<button
onClick={() => setShowAddCertModal(true)}
className="btn btn-primary flex items-center"
>
<Plus className="h-4 w-4 mr-2" />
Generate
</button>
</div>
<div className="space-y-2">
{certificates.length > 0 ? (
certificates.map((cert, index) => (
<div key={index} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<div>
<div className="text-sm font-medium text-gray-900">{cert.common_name}</div>
<div className="text-xs text-gray-500">
Valid until {formatDate(cert.not_valid_after)}
{cert.expired && <span className="text-danger-600 ml-2">(Expired)</span>}
</div>
</div>
<div className="flex items-center space-x-2">
{cert.encrypted && (
<span className="status-indicator status-online">Encrypted</span>
)}
<button
onClick={() => handleRevokeCertificate(cert.common_name)}
className="text-danger-600 hover:text-danger-900"
title="Revoke Certificate"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))
) : (
<p className="text-gray-500 text-sm">No certificates generated</p>
)}
</div>
</div>
{/* Trusted Keys */}
<div className="card">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<Key className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Trusted Keys</h3>
</div>
<button
onClick={() => setShowAddKeyModal(true)}
className="btn btn-primary flex items-center"
>
<Plus className="h-4 w-4 mr-2" />
Add Key
</button>
</div>
<div className="space-y-2">
{Object.keys(trustedKeys).length > 0 ? (
Object.entries(trustedKeys).map(([name, key]) => (
<div key={name} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<div>
<div className="text-sm font-medium text-gray-900">{name}</div>
<div className="text-xs text-gray-500">
{key.public_key.substring(0, 20)}...
<span className={`ml-2 status-indicator ${key.verified ? 'status-online' : 'status-warning'}`}>
{key.trust_level}
</span>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleRemoveTrustedKey(name)}
className="text-danger-600 hover:text-danger-900"
title="Remove Key"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))
) : (
<p className="text-gray-500 text-sm">No trusted keys configured</p>
)}
</div>
</div>
</div>
{/* Trust Chains */}
{Object.keys(trustChains).length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Trust Chains</h3>
<div className="card">
<div className="space-y-2">
{Object.entries(trustChains).map(([peer, chain]) => (
<div key={peer} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<div>
<div className="text-sm font-medium text-gray-900">{peer}</div>
<div className="text-xs text-gray-500">
Verified: {formatDate(chain.verified_at)}
</div>
</div>
<span className={`status-indicator ${chain.trust_level === 'direct' ? 'status-online' : 'status-warning'}`}>
{chain.trust_level}
</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Generate Certificate Modal */}
{showAddCertModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Generate Certificate</h3>
<form onSubmit={handleGenerateCertificate}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Common Name
</label>
<input
type="text"
value={newCert.common_name}
onChange={(e) => setNewCert({ ...newCert, common_name: e.target.value })}
className="input"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Domains (comma-separated)
</label>
<input
type="text"
value={newCert.domains}
onChange={(e) => setNewCert({ ...newCert, domains: e.target.value })}
className="input"
placeholder="example.com, www.example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Key Size
</label>
<select
value={newCert.key_size}
onChange={(e) => setNewCert({ ...newCert, key_size: parseInt(e.target.value) })}
className="input"
>
<option value={2048}>2048 bits</option>
<option value={4096}>4096 bits</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Validity (days)
</label>
<input
type="number"
value={newCert.days}
onChange={(e) => setNewCert({ ...newCert, days: parseInt(e.target.value) })}
className="input"
min="1"
max="3650"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => setShowAddCertModal(false)}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Generate
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Add Trusted Key Modal */}
{showAddKeyModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Add Trusted Key</h3>
<form onSubmit={handleAddTrustedKey}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
value={newKey.name}
onChange={(e) => setNewKey({ ...newKey, name: e.target.value })}
className="input"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key
</label>
<textarea
value={newKey.public_key}
onChange={(e) => setNewKey({ ...newKey, public_key: e.target.value })}
className="input"
rows="3"
placeholder="age1..."
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Trust Level
</label>
<select
value={newKey.trust_level}
onChange={(e) => setNewKey({ ...newKey, trust_level: e.target.value })}
className="input"
>
<option value="direct">Direct</option>
<option value="indirect">Indirect</option>
<option value="verified">Verified</option>
</select>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => setShowAddKeyModal(false)}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Add Key
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
}
export default Vault;
+94
View File
@@ -0,0 +1,94 @@
import { useState, useEffect } from 'react';
import { Shield, Key, Users } from 'lucide-react';
import { wireguardAPI } from '../services/api';
function WireGuard() {
const [status, setStatus] = useState(null);
const [peers, setPeers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchWireGuardData();
}, []);
const fetchWireGuardData = async () => {
try {
const [statusResponse, peersResponse] = await Promise.all([
wireguardAPI.getStatus(),
wireguardAPI.getPeers()
]);
setStatus(statusResponse.data);
setPeers(peersResponse.data);
} catch (error) {
console.error('Failed to fetch WireGuard data:', error);
} finally {
setIsLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
<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>
{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="flex justify-between">
<span className="text-sm text-gray-500">Status:</span>
<span className="text-sm font-medium text-success-600">Active</span>
</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>
);
}
export default WireGuard;