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