init
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user