mirror of
https://github.com/crivion/laranode.git
synced 2026-06-20 20:06:03 +08:00
firewall: move add rule to modal for ux consistency
This commit is contained in:
@@ -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.');
|
||||
|
||||
@@ -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 }) {
|
||||
<Head title="Firewall" />
|
||||
|
||||
<div className="max-w-7xl px-4 my-8">
|
||||
<form onSubmit={addRule} className="bg-white dark:bg-gray-850 p-4 rounded-md flex items-center space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
placeholder="e.g. 22/tcp or proto tcp from 1.2.3.4 to any port 22"
|
||||
value={newRule}
|
||||
onChange={(e) => setNewRule(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
value={ruleType}
|
||||
onChange={(e) => setRuleType(e.target.value)}
|
||||
>
|
||||
<option value="allow">Allow</option>
|
||||
<option value="deny">Deny</option>
|
||||
</select>
|
||||
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">Add Rule</button>
|
||||
</form>
|
||||
<div className="bg-white dark:bg-gray-850 p-4 rounded-md flex items-center justify-between">
|
||||
<CreateFirewallRuleForm />
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-x-auto bg-white dark:bg-gray-850 mt-3">
|
||||
<table className="w-full text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
|
||||
161
resources/js/Pages/Firewall/Partials/CreateFirewallRuleForm.jsx
Normal file
161
resources/js/Pages/Firewall/Partials/CreateFirewallRuleForm.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<button onClick={openModal} className='flex items-center text-gray-700 dark:text-gray-300'>
|
||||
<MdSecurity className='mr-2' />
|
||||
Add Firewall Rule
|
||||
</button>
|
||||
|
||||
<Modal show={showModal} onClose={closeModal}>
|
||||
<form onSubmit={submit} className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<MdSecurity className='mr-2' />
|
||||
Add Firewall Rule
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 flex flex-col space-y-4 max-h-[500px]">
|
||||
<div>
|
||||
<InputLabel htmlFor="type" value="Action" className='my-2' />
|
||||
<select
|
||||
id="type"
|
||||
name="type"
|
||||
value={data.type}
|
||||
onChange={(e) => setData('type', e.target.value)}
|
||||
className="mt-1 block w-full flex-1 bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
<option value="allow">Allow</option>
|
||||
<option value="deny">Deny</option>
|
||||
</select>
|
||||
<InputError message={errors.type} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="protocol" value="Protocol" className='my-2' />
|
||||
<select
|
||||
id="protocol"
|
||||
name="protocol"
|
||||
value={data.protocol}
|
||||
onChange={(e) => setData('protocol', e.target.value)}
|
||||
className="mt-1 block w-full flex-1 bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
</select>
|
||||
<InputError message={errors.protocol} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="port" value="Port" className='my-2' />
|
||||
<TextInput
|
||||
id="port"
|
||||
name="port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={data.port}
|
||||
onChange={(e) => setData('port', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="22"
|
||||
required
|
||||
/>
|
||||
<InputError message={errors.port} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="ip" value="IP" className='my-2' />
|
||||
<TextInput
|
||||
id="ip"
|
||||
name="ip"
|
||||
value={data.ip}
|
||||
onChange={(e) => setData('ip', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="any or 1.2.3.4 or 1.2.3.0/24"
|
||||
/>
|
||||
<InputError message={errors.ip} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="to" value="To" className='my-2' />
|
||||
<TextInput
|
||||
id="to"
|
||||
name="to"
|
||||
value={data.to}
|
||||
onChange={(e) => setData('to', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="any"
|
||||
/>
|
||||
<InputError message={errors.to} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="comment" value="Comment" className='my-2' />
|
||||
<TextInput
|
||||
id="comment"
|
||||
name="comment"
|
||||
value={data.comment}
|
||||
onChange={(e) => setData('comment', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<InputError message={errors.comment} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<PrimaryButton className="mr-3" disabled={processing}>
|
||||
Add Rule
|
||||
</PrimaryButton>
|
||||
<SecondaryButton onClick={closeModal}>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user