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
+42
View File
@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
+133
View File
@@ -0,0 +1,133 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useState, useEffect } from 'react';
import {
Home,
Users,
Network,
Shield,
Mail,
Calendar as CalendarIcon,
FolderOpen,
Activity,
Wifi,
Server,
Key,
Package2,
Settings as SettingsIcon
} from 'lucide-react';
import { healthAPI } from './services/api';
import Sidebar from './components/Sidebar';
import Dashboard from './pages/Dashboard';
import Peers from './pages/Peers';
import NetworkServices from './pages/NetworkServices';
import WireGuard from './pages/WireGuard';
import Email from './pages/Email';
import Calendar from './pages/Calendar';
import Files from './pages/Files';
import Routing from './pages/Routing';
import Logs from './pages/Logs';
import Settings from './pages/Settings';
import Vault from './pages/Vault';
import ContainerDashboard from './components/ContainerDashboard';
function App() {
const [isOnline, setIsOnline] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const checkHealth = async () => {
try {
await healthAPI.check();
setIsOnline(true);
} catch (error) {
console.error('Backend not available:', error);
setIsOnline(false);
} finally {
setIsLoading(false);
}
};
checkHealth();
const interval = setInterval(checkHealth, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []);
const navigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'Peers', href: '/peers', icon: Users },
{ name: 'Network Services', href: '/network', icon: Network },
{ name: 'WireGuard', href: '/wireguard', icon: Shield },
{ name: 'Email', href: '/email', icon: Mail },
{ name: 'Calendar', href: '/calendar', icon: CalendarIcon },
{ name: 'Files', href: '/files', icon: FolderOpen },
{ name: 'Routing', href: '/routing', icon: Wifi },
{ name: 'Vault', href: '/vault', icon: Key },
{ name: 'Containers', href: '/containers', icon: Package2 },
{ name: 'Logs', href: '/logs', icon: Activity },
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
];
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Connecting to Personal Internet Cell...</p>
</div>
</div>
);
}
return (
<Router>
<div className="min-h-screen bg-gray-50">
<Sidebar navigation={navigation} isOnline={isOnline} />
<div className="lg:pl-72">
<main className="py-10">
<div className="px-4 sm:px-6 lg:px-8">
{!isOnline && (
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<Server className="h-5 w-5 text-danger-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-danger-800">
Backend Unavailable
</h3>
<div className="mt-2 text-sm text-danger-700">
<p>
Unable to connect to the Personal Internet Cell backend.
Please ensure the API server is running on port 3000.
</p>
</div>
</div>
</div>
</div>
)}
<Routes>
<Route path="/" element={<Dashboard isOnline={isOnline} />} />
<Route path="/peers" element={<Peers />} />
<Route path="/network" element={<NetworkServices />} />
<Route path="/wireguard" element={<WireGuard />} />
<Route path="/email" element={<Email />} />
<Route path="/calendar" element={<Calendar />} />
<Route path="/files" element={<Files />} />
<Route path="/routing" element={<Routing />} />
<Route path="/vault" element={<Vault />} />
<Route path="/containers" element={<ContainerDashboard />} />
<Route path="/logs" element={<Logs />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</div>
</main>
</div>
</div>
</Router>
);
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+328
View File
@@ -0,0 +1,328 @@
import React, { useEffect, useState } from 'react';
import { containerAPI } from '../services/api';
import { vaultAPI } from '../services/api';
const ContainerDashboard = () => {
const [containers, setContainers] = useState([]);
const [images, setImages] = useState([]);
const [volumes, setVolumes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [logs, setLogs] = useState('');
const [selectedContainer, setSelectedContainer] = useState(null);
const [stats, setStats] = useState(null);
// Form states
const [newContainer, setNewContainer] = useState({ image: '', name: '', env: '', ports: '', volumes: '', command: '' });
const [pullImageName, setPullImageName] = useState('');
const [newVolumeName, setNewVolumeName] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [secrets, setSecrets] = useState([]);
const [newSecret, setNewSecret] = useState({ name: '', value: '' });
const [selectedSecrets, setSelectedSecrets] = useState([]);
const fetchAll = async () => {
setLoading(true);
setError('');
try {
const [cRes, iRes, vRes] = await Promise.all([
containerAPI.listContainers(),
containerAPI.listImages(),
containerAPI.listVolumes(),
]);
setContainers(cRes.data);
setImages(iRes.data);
setVolumes(vRes.data);
} catch (e) {
setError('Failed to load data');
}
setLoading(false);
};
const fetchSecrets = async () => {
try {
const res = await vaultAPI.listSecrets();
setSecrets(res.data.secrets || []);
} catch (e) {
setError('Failed to load secrets');
}
};
useEffect(() => {
fetchAll();
fetchSecrets();
}, []);
const handleAction = async (action, name) => {
setError('');
setActionLoading(true);
try {
if (action === 'start') await containerAPI.startContainer(name);
if (action === 'stop') await containerAPI.stopContainer(name);
if (action === 'restart') await containerAPI.restartContainer(name);
if (action === 'remove') await containerAPI.removeContainer(name);
fetchAll();
} catch (e) {
setError(`Failed to ${action} container: ${name}`);
}
setActionLoading(false);
};
const handleShowLogs = async (name) => {
setError('');
setLogs('Loading...');
setSelectedContainer(name);
try {
const res = await containerAPI.getContainerLogs(name);
setLogs(res.data.logs);
} catch (e) {
setLogs('Failed to load logs');
}
};
const handleShowStats = async (name) => {
setError('');
setStats('Loading...');
setSelectedContainer(name);
try {
const res = await containerAPI.getContainerStats(name);
setStats(res.data);
} catch (e) {
setStats('Failed to load stats');
}
};
const handleAddSecret = async (e) => {
e.preventDefault();
setError('');
setActionLoading(true);
try {
await vaultAPI.storeSecret(newSecret.name, newSecret.value);
setNewSecret({ name: '', value: '' });
fetchSecrets();
} catch (e) {
setError('Failed to add secret');
}
setActionLoading(false);
};
const handleDeleteSecret = async (name) => {
setError('');
setActionLoading(true);
try {
await vaultAPI.deleteSecret(name);
fetchSecrets();
} catch (e) {
setError('Failed to delete secret');
}
setActionLoading(false);
};
const handleSecretSelect = (e) => {
const value = e.target.value;
setSelectedSecrets(
e.target.checked
? [...selectedSecrets, value]
: selectedSecrets.filter((s) => s !== value)
);
};
const handleCreateContainer = async (e) => {
e.preventDefault();
setError('');
setActionLoading(true);
try {
// Parse env, ports, volumes from string to object
const env = newContainer.env ? Object.fromEntries(newContainer.env.split(',').map(pair => pair.split('='))) : {};
const ports = newContainer.ports ? Object.fromEntries(newContainer.ports.split(',').map(pair => pair.split(':'))) : {};
const volumes = newContainer.volumes ? Object.fromEntries(newContainer.volumes.split(',').map(pair => pair.split(':'))) : {};
const data = {
image: newContainer.image,
name: newContainer.name,
env,
ports,
volumes,
command: newContainer.command,
secrets: selectedSecrets
};
const res = await containerAPI.createContainer(data);
if (res.data.error) setError(res.data.error);
setNewContainer({ image: '', name: '', env: '', ports: '', volumes: '', command: '' });
setSelectedSecrets([]);
fetchAll();
} catch (e) {
setError('Failed to create container');
}
setActionLoading(false);
};
const handlePullImage = async (e) => {
e.preventDefault();
setError('');
setActionLoading(true);
try {
const res = await containerAPI.pullImage(pullImageName);
if (res.data.error) setError(res.data.error);
setPullImageName('');
fetchAll();
} catch (e) {
setError('Failed to pull image');
}
setActionLoading(false);
};
const handleRemoveImage = async (image) => {
setError('');
setActionLoading(true);
try {
await containerAPI.removeImage(image);
fetchAll();
} catch (e) {
setError('Failed to remove image');
}
setActionLoading(false);
};
const handleCreateVolume = async (e) => {
e.preventDefault();
setError('');
setActionLoading(true);
try {
const res = await containerAPI.createVolume(newVolumeName);
if (res.data.error) setError(res.data.error);
setNewVolumeName('');
fetchAll();
} catch (e) {
setError('Failed to create volume');
}
setActionLoading(false);
};
const handleRemoveVolume = async (name) => {
setError('');
setActionLoading(true);
try {
await containerAPI.removeVolume(name);
fetchAll();
} catch (e) {
setError('Failed to remove volume');
}
setActionLoading(false);
};
return (
<div style={{ padding: 24 }}>
<h2>Container Management Dashboard</h2>
{loading ? <p>Loading...</p> : null}
{error && <p style={{ color: 'red' }}>{error}</p>}
<h3>Secrets</h3>
<form onSubmit={handleAddSecret} style={{ marginBottom: 8 }}>
<strong>Add Secret:</strong>
<input required placeholder="Name" value={newSecret.name} onChange={e => setNewSecret({ ...newSecret, name: e.target.value })} />
<input required placeholder="Value" value={newSecret.value} onChange={e => setNewSecret({ ...newSecret, value: e.target.value })} />
<button type="submit" disabled={actionLoading}>Add</button>
</form>
<ul>
{secrets.map((s) => (
<li key={s}>
{s}
<button onClick={() => handleDeleteSecret(s)} disabled={actionLoading} style={{ marginLeft: 8 }}>Delete</button>
</li>
))}
</ul>
<h3>Containers</h3>
<form onSubmit={handleCreateContainer} style={{ marginBottom: 16 }}>
<strong>Create Container:</strong>
<input required placeholder="Image" value={newContainer.image} onChange={e => setNewContainer({ ...newContainer, image: e.target.value })} />
<input placeholder="Name" value={newContainer.name} onChange={e => setNewContainer({ ...newContainer, name: e.target.value })} />
<input placeholder="Env (KEY=VAL,...)" value={newContainer.env} onChange={e => setNewContainer({ ...newContainer, env: e.target.value })} />
<input placeholder="Ports (host:container,...)" value={newContainer.ports} onChange={e => setNewContainer({ ...newContainer, ports: e.target.value })} />
<input placeholder="Volumes (host:container,...)" value={newContainer.volumes} onChange={e => setNewContainer({ ...newContainer, volumes: e.target.value })} />
<input placeholder="Command" value={newContainer.command} onChange={e => setNewContainer({ ...newContainer, command: e.target.value })} />
<div style={{ margin: '8px 0' }}>
<strong>Attach Secrets:</strong>
{secrets.map((s) => (
<label key={s} style={{ marginLeft: 8 }}>
<input type="checkbox" value={s} checked={selectedSecrets.includes(s)} onChange={handleSecretSelect} /> {s}
</label>
))}
</div>
<button type="submit" disabled={actionLoading}>Create</button>
</form>
<table border="1" cellPadding="6">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Image</th>
<th>Actions</th>
<th>Logs</th>
<th>Stats</th>
</tr>
</thead>
<tbody>
{containers.map((c) => (
<tr key={c.id}>
<td>{c.name}</td>
<td>{c.status}</td>
<td>{c.image && c.image.join(', ')}</td>
<td>
<button onClick={() => handleAction('start', c.name)} disabled={actionLoading}>Start</button>
<button onClick={() => handleAction('stop', c.name)} disabled={actionLoading}>Stop</button>
<button onClick={() => handleAction('restart', c.name)} disabled={actionLoading}>Restart</button>
<button onClick={() => handleAction('remove', c.name)} disabled={actionLoading}>Remove</button>
</td>
<td>
<button onClick={() => handleShowLogs(c.name)}>Show Logs</button>
</td>
<td>
<button onClick={() => handleShowStats(c.name)}>Show Stats</button>
</td>
</tr>
))}
</tbody>
</table>
{selectedContainer && logs && (
<div style={{ marginTop: 16 }}>
<h4>Logs for {selectedContainer}</h4>
<pre style={{ background: '#222', color: '#eee', padding: 12, maxHeight: 300, overflow: 'auto' }}>{logs}</pre>
</div>
)}
{selectedContainer && stats && (
<div style={{ marginTop: 16 }}>
<h4>Stats for {selectedContainer}</h4>
<pre style={{ background: '#222', color: '#eee', padding: 12, maxHeight: 300, overflow: 'auto' }}>{typeof stats === 'string' ? stats : JSON.stringify(stats, null, 2) }</pre>
</div>
)}
<h3>Images</h3>
<form onSubmit={handlePullImage} style={{ marginBottom: 8 }}>
<strong>Pull Image:</strong>
<input required placeholder="Image name" value={pullImageName} onChange={e => setPullImageName(e.target.value)} />
<button type="submit" disabled={actionLoading}>Pull</button>
</form>
<ul>
{images.map((img) => (
<li key={img.id}>
{img.tags && img.tags.join(', ')} ({img.short_id})
<button onClick={() => handleRemoveImage(img.id)} disabled={actionLoading} style={{ marginLeft: 8 }}>Remove</button>
</li>
))}
</ul>
<h3>Volumes</h3>
<form onSubmit={handleCreateVolume} style={{ marginBottom: 8 }}>
<strong>Create Volume:</strong>
<input required placeholder="Volume name" value={newVolumeName} onChange={e => setNewVolumeName(e.target.value)} />
<button type="submit" disabled={actionLoading}>Create</button>
</form>
<ul>
{volumes.map((v) => (
<li key={v.name}>
{v.name} ({v.mountpoint})
<button onClick={() => handleRemoveVolume(v.name)} disabled={actionLoading} style={{ marginLeft: 8 }}>Remove</button>
</li>
))}
</ul>
</div>
);
};
export default ContainerDashboard;
+149
View File
@@ -0,0 +1,149 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { X } from 'lucide-react';
import { clsx } from 'clsx';
function Sidebar({ navigation, isOnline }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const location = useLocation();
return (
<>
{/* Mobile sidebar */}
<div className={clsx(
'fixed inset-0 z-50 lg:hidden',
sidebarOpen ? 'block' : 'hidden'
)}>
<div className="fixed inset-0 bg-gray-900/80" onClick={() => setSidebarOpen(false)} />
<div className="fixed inset-y-0 left-0 z-50 w-72 bg-white">
<div className="flex h-full flex-col gap-y-5 overflow-y-auto px-6 py-4">
<div className="flex h-16 shrink-0 items-center">
<h1 className="text-xl font-semibold text-gray-900">
Personal Internet Cell
</h1>
<button
type="button"
className="ml-auto"
onClick={() => setSidebarOpen(false)}
>
<X className="h-6 w-6" />
</button>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
to={item.href}
className={clsx(
location.pathname === item.href
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:text-primary-600 hover:bg-primary-50',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}
onClick={() => setSidebarOpen(false)}
>
<item.icon
className={clsx(
location.pathname === item.href ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-600',
'h-6 w-6 shrink-0'
)}
aria-hidden="true"
/>
{item.name}
</Link>
</li>
))}
</ul>
</li>
</ul>
</nav>
</div>
</div>
</div>
{/* Desktop sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
<div className="flex h-16 shrink-0 items-center">
<h1 className="text-xl font-semibold text-gray-900">
Personal Internet Cell
</h1>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
to={item.href}
className={clsx(
location.pathname === item.href
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:text-primary-600 hover:bg-primary-50',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}
>
<item.icon
className={clsx(
location.pathname === item.href ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-600',
'h-6 w-6 shrink-0'
)}
aria-hidden="true"
/>
{item.name}
</Link>
</li>
))}
</ul>
</li>
<li className="mt-auto">
<div className="flex items-center gap-x-2">
<div className={clsx(
'h-2 w-2 rounded-full',
isOnline ? 'bg-success-500' : 'bg-danger-500'
)} />
<span className="text-xs text-gray-500">
{isOnline ? 'Connected' : 'Disconnected'}
</span>
</div>
</li>
</ul>
</nav>
</div>
</div>
{/* Mobile menu button */}
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<button
type="button"
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
<div className="flex-1 text-sm font-semibold leading-6 text-gray-900">
Personal Internet Cell
</div>
<div className="flex items-center gap-x-2">
<div className={clsx(
'h-2 w-2 rounded-full',
isOnline ? 'bg-success-500' : 'bg-danger-500'
)} />
<span className="text-xs text-gray-500">
{isOnline ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
</>
);
}
export default Sidebar;
+59
View File
@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
}
body {
@apply bg-gray-50 text-gray-900;
}
}
@layer components {
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
}
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700;
}
.btn-secondary {
@apply bg-gray-200 text-gray-800 hover:bg-gray-300;
}
.btn-danger {
@apply bg-danger-600 text-white hover:bg-danger-700;
}
.btn-success {
@apply bg-success-600 text-white hover:bg-success-700;
}
.card {
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6;
}
.input {
@apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
}
.status-indicator {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.status-online {
@apply bg-success-100 text-success-800;
}
.status-offline {
@apply bg-danger-100 text-danger-800;
}
.status-warning {
@apply bg-warning-100 text-warning-800;
}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+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;
+205
View File
@@ -0,0 +1,205 @@
import axios from 'axios';
// Create axios instance with base configuration
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for logging
api.interceptors.request.use(
(config) => {
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => {
console.error('API Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor for error handling
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error('API Response Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
// Cell Status API
export const cellAPI = {
getStatus: () => api.get('/api/status'),
getConfig: () => api.get('/api/config'),
updateConfig: (config) => api.put('/api/config', config),
};
// Network Services API
export const networkAPI = {
getDNSRecords: () => api.get('/api/dns/records'),
addDNSRecord: (record) => api.post('/api/dns/records', record),
removeDNSRecord: (record) => api.delete('/api/dns/records', { data: record }),
getDHCPLeases: () => api.get('/api/dhcp/leases'),
addDHCPReservation: (reservation) => api.post('/api/dhcp/reservations', reservation),
removeDHCPReservation: (reservation) => api.delete('/api/dhcp/reservations', { data: reservation }),
getNTPStatus: () => api.get('/api/ntp/status'),
testNetwork: (data) => api.post('/api/network/test', data),
};
// WireGuard API
export const wireguardAPI = {
getKeys: () => api.get('/api/wireguard/keys'),
generatePeerKeys: (data) => api.post('/api/wireguard/keys/peer', data),
getConfig: () => api.get('/api/wireguard/config'),
getPeers: () => api.get('/api/wireguard/peers'),
addPeer: (peer) => api.post('/api/wireguard/peers', peer),
removePeer: (peer) => api.delete('/api/wireguard/peers', { data: peer }),
getStatus: () => api.get('/api/wireguard/status'),
testConnectivity: (data) => api.post('/api/wireguard/connectivity', data),
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
};
// Peer Registry API
export const peerAPI = {
getPeers: () => api.get('/api/peers'),
addPeer: (peer) => api.post('/api/peers', peer),
removePeer: (peerName) => api.delete(`/api/peers/${peerName}`),
registerPeer: (data) => api.post('/api/peers/register', data),
unregisterPeer: (peerName) => api.delete(`/api/peers/${peerName}/unregister`),
updatePeerIP: (peerName, data) => api.put(`/api/peers/${peerName}/update-ip`, data),
};
// Email Services API
export const emailAPI = {
getUsers: () => api.get('/api/email/users'),
createUser: (user) => api.post('/api/email/users', user),
deleteUser: (username) => api.delete(`/api/email/users/${username}`),
getStatus: () => api.get('/api/email/status'),
testConnectivity: () => api.get('/api/email/connectivity'),
sendEmail: (data) => api.post('/api/email/send', data),
getMailboxInfo: (username) => api.get(`/api/email/mailbox/${username}`),
};
// Calendar Services API
export const calendarAPI = {
getUsers: () => api.get('/api/calendar/users'),
createUser: (user) => api.post('/api/calendar/users', user),
deleteUser: (username) => api.delete(`/api/calendar/users/${username}`),
createCalendar: (data) => api.post('/api/calendar/calendars', data),
addEvent: (data) => api.post('/api/calendar/events', data),
getEvents: (username, calendarName, params) =>
api.get(`/api/calendar/events/${username}/${calendarName}`, { params }),
getStatus: () => api.get('/api/calendar/status'),
testConnectivity: () => api.get('/api/calendar/connectivity'),
};
// File Services API
export const fileAPI = {
getUsers: () => api.get('/api/files/users'),
createUser: (user) => api.post('/api/files/users', user),
deleteUser: (username) => api.delete(`/api/files/users/${username}`),
createFolder: (data) => api.post('/api/files/folders', data),
deleteFolder: (username, folderPath) => api.delete(`/api/files/folders/${username}/${folderPath}`),
uploadFile: (username, file, path) => {
const formData = new FormData();
formData.append('file', file);
formData.append('path', path);
return api.post(`/api/files/upload/${username}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
},
downloadFile: (username, filePath) => api.get(`/api/files/download/${username}/${filePath}`),
deleteFile: (username, filePath) => api.delete(`/api/files/delete/${username}/${filePath}`),
listFiles: (username, folder = '') => api.get(`/api/files/list/${username}`, { params: { folder } }),
getStatus: () => api.get('/api/files/status'),
testConnectivity: () => api.get('/api/files/connectivity'),
};
// Routing API
export const routingAPI = {
getStatus: () => api.get('/api/routing/status'),
// NAT
getNatRules: () => api.get('/api/routing/nat'),
addNatRule: (rule) => api.post('/api/routing/nat', rule),
deleteNatRule: (ruleId) => api.delete(`/api/routing/nat/${ruleId}`),
// Peer Routes
getPeerRoutes: () => api.get('/api/routing/peers'),
addPeerRoute: (route) => api.post('/api/routing/peers', route),
deletePeerRoute: (peerName) => api.delete(`/api/routing/peers/${peerName}`),
// Firewall
getFirewallRules: () => api.get('/api/routing/firewall'),
addFirewallRule: (rule) => api.post('/api/routing/firewall', rule),
deleteFirewallRule: (ruleId) => api.delete(`/api/routing/firewall/${ruleId}`),
// Other
addExitNode: (node) => api.post('/api/routing/exit-nodes', node),
addBridgeRoute: (route) => api.post('/api/routing/bridge', route),
addSplitRoute: (route) => api.post('/api/routing/split', route),
testConnectivity: (data) => api.post('/api/routing/connectivity', data),
getLogs: (lines = 50) => api.get('/api/routing/logs', { params: { lines } }),
};
// Vault & Trust API
export const vaultAPI = {
getStatus: () => api.get('/api/vault/status'),
getCertificates: () => api.get('/api/vault/certificates'),
generateCertificate: (data) => api.post('/api/vault/certificates', data),
revokeCertificate: (commonName) => api.delete(`/api/vault/certificates/${commonName}`),
getCACertificate: () => api.get('/api/vault/ca/certificate'),
getAgePublicKey: () => api.get('/api/vault/age/public-key'),
getTrustedKeys: () => api.get('/api/vault/trust/keys'),
addTrustedKey: (data) => api.post('/api/vault/trust/keys', data),
removeTrustedKey: (name) => api.delete(`/api/vault/trust/keys/${name}`),
verifyTrustChain: (data) => api.post('/api/vault/trust/verify', data),
getTrustChains: () => api.get('/api/vault/trust/chains'),
// Secrets management
listSecrets: () => api.get('/api/vault/secrets'),
storeSecret: (name, value) => api.post('/api/vault/secrets', { name, value }),
getSecret: (name) => api.get(`/api/vault/secrets/${name}`),
deleteSecret: (name) => api.delete(`/api/vault/secrets/${name}`),
};
// Services API
export const servicesAPI = {
getAllStatus: () => api.get('/api/services/status'),
testAllConnectivity: () => api.get('/api/services/connectivity'),
};
// Health check
export const healthAPI = {
check: () => api.get('/health'),
};
// Monitoring API
export const monitoringAPI = {
getBackendLogs: (lines = 100) => api.get('/api/logs', { params: { lines } }),
getHealthHistory: () => api.get('/api/health/history'),
};
// Container Management API
export const containerAPI = {
// Containers
listContainers: () => api.get('/api/containers'),
startContainer: (name) => api.post(`/api/containers/${name}/start`),
stopContainer: (name) => api.post(`/api/containers/${name}/stop`),
restartContainer: (name) => api.post(`/api/containers/${name}/restart`),
getContainerLogs: (name, tail = 100) => api.get(`/api/containers/${name}/logs`, { params: { tail } }),
getContainerStats: (name) => api.get(`/api/containers/${name}/stats`),
createContainer: (data) => api.post('/api/containers', data), // data may include 'secrets' array
removeContainer: (name, force = false) => api.delete(`/api/containers/${name}`, { params: { force } }),
// Images
listImages: () => api.get('/api/images'),
pullImage: (image) => api.post('/api/images/pull', { image }),
removeImage: (image, force = false) => api.delete(`/api/images/${image}`, { params: { force } }),
// Volumes
listVolumes: () => api.get('/api/volumes'),
createVolume: (name) => api.post('/api/volumes', { name }),
removeVolume: (name, force = false) => api.delete(`/api/volumes/${name}`, { params: { force } }),
};
export default api;