feat(iplimit): gate IP limit on fail2ban and reset stale limits

Per-client IP limit only enforces where fail2ban is installed, so the panel now reports enforceability and disables the field otherwise:

- Add GET /panel/api/server/fail2banStatus (enabled/installed/usable/windows), cached 30s.
- ClientFormModal and ClientBulkAddModal disable the IP Limit input when not usable and show a hover tooltip; Windows gets a platform-specific message instead of the bash-menu hint.
- One-time migration ResetIpLimitNoFail2ban zeroes existing client limitIp (inbound settings JSON + clients table) on hosts without fail2ban, where the limit never applied.
- Drop the recurring '[LimitIP] Fail2Ban is not installed' warning.
- Add limitIpFail2banMissing/limitIpFail2banWindows/limitIpDisabled across all 13 locales.
This commit is contained in:
MHSanaei
2026-06-22 23:15:58 +02:00
parent 718b7e16e1
commit ce8b1bed77
23 changed files with 315 additions and 16 deletions
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AutoComplete, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, message } from 'antd';
import { AutoComplete, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tooltip, message } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
@@ -10,6 +10,7 @@ import { formatInboundLabel } from '@/lib/inbounds/label';
import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
import { DateTimePicker, SelectAllClearButtons } from '@/components/form';
import { useClients, type InboundOption } from '@/hooks/useClients';
import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2banStatusQuery';
import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas/client';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
@@ -62,6 +63,9 @@ export default function ClientBulkAddModal({
const [form, setForm] = useState<FormState>(emptyForm);
const [delayedStart, setDelayedStart] = useState(false);
const [saving, setSaving] = useState(false);
const fail2ban = useFail2banStatusQuery();
const limitIpDisabled = !fail2ban.usable;
const limitIpNotice = getLimitIpNotice(fail2ban, t);
useEffect(() => {
if (!open) return;
@@ -311,7 +315,13 @@ export default function ClientBulkAddModal({
)}
<Form.Item label={t('pages.clients.limitIp')}>
<InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
<Tooltip title={limitIpNotice || undefined}>
<span style={{ display: 'inline-flex' }}>
<InputNumber value={form.limitIp} min={0} disabled={limitIpDisabled}
style={limitIpDisabled ? { pointerEvents: 'none' } : undefined}
onChange={(v) => update('limitIp', Number(v) || 0)} />
</span>
</Tooltip>
</Form.Item>
<Form.Item label={t('pages.clients.totalGB')}>