feat(ui): validate the REALITY client version range at save time (#6126)

* feat(ui): validate the REALITY client version range at save time

The impossible range from PR #6125 — a max below the effective minimum
— could still be saved; the tooltip only helps a user who hovers it.
Add save-time validation mirroring xray-core's parser (up to three
dot-separated parts, each 0-255) on both fields, plus a cross-field
check that a non-empty max is not below a non-empty min. Errors are
field-level i18n keys following the REALITY target precedent, so the
modal stays open and points at the offending field instead of storing
a config that rejects every client.

A malformed min is reported by its own field and skipped by the max
comparison, so the user sees one precise error per field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): reject untrimmed client versions and revalidate max on min edits

From review: the validators trimmed but the save path ships the value
verbatim, and xray-core's part parser accepts no surrounding
whitespace — so a green form could still save a config the core
refuses to load. Reject any value that differs from its trimmed form.

Also revalidate the max field after a min edit when max already
shows an error, so correcting the min clears the stale cross-field
message without waiting for the next submit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
PathGao
2026-07-29 05:04:03 +08:00
committed by GitHub
parent 411271b454
commit ca6955d88b
16 changed files with 171 additions and 1 deletions
@@ -1,11 +1,16 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Collapse, Descriptions, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { RadarChartOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { FormField } from '@/components/form/rhf';
import { UTLS_FINGERPRINT } from '@/schemas/primitives';
import { validateRealityTarget } from '@/lib/xray/stream-wire-normalize';
import {
validateRealityClientVer,
validateRealityMaxClientVer,
validateRealityTarget,
} from '@/lib/xray/stream-wire-normalize';
import type { RealityScanResult } from '@/generated/types';
import RealityTargetScannerModal from './RealityTargetScannerModal';
@@ -39,7 +44,14 @@ export default function RealityForm({
clearMldsa65,
}: RealityFormProps) {
const { t } = useTranslation();
const { getFieldState, trigger } = useFormContext();
const [scannerOpen, setScannerOpen] = useState(false);
const maxClientVerPath = 'streamSettings.realitySettings.maxClientVer';
const revalidateMaxClientVer = () => {
if (getFieldState(maxClientVerPath).error) {
void trigger(maxClientVerPath);
}
};
return (
<>
<FormField
@@ -128,6 +140,13 @@ export default function RealityForm({
name={['streamSettings', 'realitySettings', 'minClientVer']}
label={t('pages.inbounds.form.minClientVer')}
tooltip={t('pages.inbounds.form.minClientVerHint')}
onAfterChange={revalidateMaxClientVer}
rules={{
validate: (value) => {
const errKey = validateRealityClientVer(typeof value === 'string' ? value : '');
return errKey ? errKey : true;
},
}}
>
<Input placeholder="26.3.27" />
</FormField>
@@ -135,6 +154,14 @@ export default function RealityForm({
name={['streamSettings', 'realitySettings', 'maxClientVer']}
label={t('pages.inbounds.form.maxClientVer')}
tooltip={t('pages.inbounds.form.maxClientVerHint')}
rules={{
validate: (value, formValues) => {
const max = typeof value === 'string' ? value : '';
const min = formValues?.streamSettings?.realitySettings?.minClientVer;
const errKey = validateRealityMaxClientVer(max, typeof min === 'string' ? min : '');
return errKey ? errKey : true;
},
}}
>
<Input placeholder="x.y.z" />
</FormField>