diff --git a/app/Http/Controllers/FirewallController.php b/app/Http/Controllers/FirewallController.php index 7d48370..586486f 100644 --- a/app/Http/Controllers/FirewallController.php +++ b/app/Http/Controllers/FirewallController.php @@ -38,14 +38,43 @@ class FirewallController extends Controller public function store(Request $request): RedirectResponse { $validated = $request->validate([ - 'rule' => 'required|string|min:2', 'type' => 'required|string|in:allow,deny', + 'protocol' => 'required|string|in:tcp,udp', + 'port' => 'required|integer|min:1|max:65535', + 'ip' => 'required|string', // validated below for any|ip|cidr + 'to' => 'required|string', + 'comment' => 'nullable|string|max:150', ]); + $from = trim($validated['ip']); + $to = trim($validated['to']); + + $isAny = fn(string $v) => strtolower($v) === 'any'; + $isIp = fn(string $v) => filter_var($v, FILTER_VALIDATE_IP) !== false; + $isCidr = fn(string $v) => (bool) preg_match('/^((25[0-5]|2[0-4]\\d|1?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|1?\\d?\\d)){3})\\/(3[0-2]|[12]?\\d)$/', $v); + + if (!($isAny($from) || $isIp($from) || $isCidr($from))) { + return back()->withErrors(['ip' => 'IP must be "any", a valid IP address, or CIDR range.'])->withInput(); + } + if (!($isAny($to) || $isIp($to))) { + return back()->withErrors(['to' => 'To must be "any" or a valid IP address.'])->withInput(); + } + + $proto = strtolower($validated['protocol']); + $port = (int) $validated['port']; + $comment = $validated['comment'] ?? ''; + $comment = trim($comment); + $commentEscaped = str_replace("'", "\\'", $comment); + + $spec = "proto {$proto} from {$from} to {$to} port {$port}"; + if ($commentEscaped !== '') { + $spec .= " comment '" . $commentEscaped . "'"; + } + if ($validated['type'] === 'allow') { - (new AddUfwRuleAction())->execute($validated['rule']); + (new AddUfwRuleAction())->execute($spec); } else { - (new \App\Actions\Firewall\AddUfwDenyRuleAction())->execute($validated['rule']); + (new \App\Actions\Firewall\AddUfwDenyRuleAction())->execute($spec); } session()->flash('success', 'Rule ' . $validated['type'] . 'ed successfully.'); diff --git a/resources/js/Pages/Firewall/Index.jsx b/resources/js/Pages/Firewall/Index.jsx index 8d7b343..9799d15 100644 --- a/resources/js/Pages/Firewall/Index.jsx +++ b/resources/js/Pages/Firewall/Index.jsx @@ -4,6 +4,7 @@ import { router } from '@inertiajs/react'; import { useState } from 'react'; import { toast } from 'react-toastify'; import ConfirmationButton from '@/Components/ConfirmationButton'; +import CreateFirewallRuleForm from './Partials/CreateFirewallRuleForm'; import { MdSecurity } from 'react-icons/md'; import { FaToggleOn, FaToggleOff } from 'react-icons/fa'; import { TiDelete } from 'react-icons/ti'; @@ -23,15 +24,7 @@ export default function FirewallIndex({ status, rules }) { }); }; - const addRule = (e) => { - e.preventDefault(); - if (!newRule.trim()) return; - router.post(route('firewall.store'), { rule: newRule.trim(), type: ruleType }, { - onBefore: () => toast('Adding rule...'), - onSuccess: () => { setNewRule(''); setRuleType('allow'); router.reload({ only: ['rules'] }); }, - onError: () => toast('Failed to add rule') - }); - }; + // rule creation moved to modal form component const deleteRule = (idOrSpec) => { router.delete(route('firewall.destroy', { id: idOrSpec }), { @@ -65,24 +58,9 @@ export default function FirewallIndex({ status, rules }) {
-
- setNewRule(e.target.value)} - /> - - -
+
+ +
diff --git a/resources/js/Pages/Firewall/Partials/CreateFirewallRuleForm.jsx b/resources/js/Pages/Firewall/Partials/CreateFirewallRuleForm.jsx new file mode 100644 index 0000000..3591f7a --- /dev/null +++ b/resources/js/Pages/Firewall/Partials/CreateFirewallRuleForm.jsx @@ -0,0 +1,161 @@ +import InputError from '@/Components/InputError'; +import InputLabel from '@/Components/InputLabel'; +import Modal from '@/Components/Modal'; +import PrimaryButton from '@/Components/PrimaryButton'; +import SecondaryButton from '@/Components/SecondaryButton'; +import TextInput from '@/Components/TextInput'; +import { useForm } from '@inertiajs/react'; +import { useState } from 'react'; +import { MdSecurity } from 'react-icons/md'; + +export default function CreateFirewallRuleForm() { + const [showModal, setShowModal] = useState(false); + + const { + data, + setData, + post, + processing, + reset, + errors, + clearErrors, + } = useForm({ + type: 'allow', + protocol: 'tcp', + port: '', + ip: 'any', + to: 'any', + comment: '', + }); + + const openModal = () => setShowModal(true); + + const closeModal = () => { + setShowModal(false); + clearErrors(); + reset({ type: 'allow', protocol: 'tcp', port: '', ip: 'any', to: 'any', comment: '' }); + }; + + const submit = (e) => { + e.preventDefault(); + post(route('firewall.store'), { + preserveScroll: true, + onSuccess: () => closeModal(), + }); + }; + + return ( + <> + + + +
+

+ + Add Firewall Rule +

+ +
+
+ + + +
+ +
+ + + +
+ +
+ + setData('port', e.target.value)} + className="mt-1 block w-full" + placeholder="22" + required + /> + +
+ +
+ + setData('ip', e.target.value)} + className="mt-1 block w-full" + placeholder="any or 1.2.3.4 or 1.2.3.0/24" + /> + +
+ +
+ + setData('to', e.target.value)} + className="mt-1 block w-full" + placeholder="any" + /> + +
+ +
+ + setData('comment', e.target.value)} + className="mt-1 block w-full" + placeholder="Optional" + /> + +
+ +
+ + Add Rule + + + Cancel + +
+
+ +
+ + ); +}