mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-13 08:35:00 +03:00
fix(hosts): let the Host form clear its SNI (#7095)
Removing the last SNI tag in Hosts → Edit Host and saving did nothing: the submit fell back to the hidden legacy `sni` field, so the old name was sent again and the backend rebuilt the pool from it. It came back on the next edit and stayed in every share link built from that host. The tag list is the only SNI input, so the primary SNI is now the first tag or empty. The dynamic ECH helper read the same stale field and follows the list too. Hosts saved with only the legacy field still load, because the form seeds the tag list from it.
This commit is contained in:
@@ -121,8 +121,7 @@ export default function HostFormModal({
|
||||
watchedSnis && watchedSnis.filter((s) => typeof s === 'string' && s.trim() !== '').length > 1,
|
||||
);
|
||||
|
||||
const watchedSingleSni = useWatch({ control: methods.control, name: 'sni' }) ?? '';
|
||||
const hostSni = (watchedSnis && watchedSnis.length > 0 ? watchedSnis[0] : '') || watchedSingleSni;
|
||||
const hostSni = watchedSnis?.[0] ?? '';
|
||||
const hostEchConfig = useWatch({ control: methods.control, name: 'echConfigList' }) ?? '';
|
||||
const showTls = security === 'tls' || security === 'reality' || security === 'same';
|
||||
const showTlsExtras = security === 'tls' || security === 'same';
|
||||
@@ -208,11 +207,10 @@ export default function HostFormModal({
|
||||
const { enable, ...rest } = values;
|
||||
const isDisabled = !enable;
|
||||
const cleanedSnis = (rest.snis || []).map((s) => s.trim()).filter(Boolean);
|
||||
const primarySni = cleanedSnis.length > 0 ? cleanedSnis[0] : rest.sni || '';
|
||||
const payload: BulkAddHostValues = {
|
||||
...rest,
|
||||
hosts: (rest.hosts || []).filter((h) => h && h.trim() !== ''),
|
||||
sni: primarySni,
|
||||
sni: cleanedSnis[0] ?? '',
|
||||
snis: cleanedSnis,
|
||||
sniSharedRandom: cleanedSnis.length > 1 && Boolean(rest.sniSharedRandom),
|
||||
isDisabled,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { HostRecord } from '@/api/queries/useHostsQuery';
|
||||
import HostFormModal from '@/pages/hosts/HostFormModal';
|
||||
import { HttpUtil, Msg } from '@/utils';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
const host = {
|
||||
groupId: 'g1',
|
||||
remark: 'DE WS',
|
||||
inboundIds: [1],
|
||||
hosts: ['cdn.example.com'],
|
||||
security: 'same',
|
||||
sni: 'dash.example.com',
|
||||
snis: ['dash.example.com'],
|
||||
} as HostRecord;
|
||||
|
||||
describe('Host form SNI', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', []));
|
||||
});
|
||||
|
||||
it('saves an empty SNI after the last SNI tag is removed', async () => {
|
||||
const save = vi.fn().mockResolvedValue({ success: true });
|
||||
renderWithProviders(
|
||||
<HostFormModal
|
||||
open
|
||||
mode="edit"
|
||||
host={host}
|
||||
inboundOptions={[{ id: 1, remark: 'in', tag: 'in', protocol: 'vless' }]}
|
||||
existingHosts={[host]}
|
||||
save={save}
|
||||
onOpenChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
const tag = await screen.findByTitle('dash.example.com');
|
||||
const remove = tag.querySelector('.ant-select-selection-item-remove');
|
||||
if (!remove) throw new Error('missing SNI tag remove button');
|
||||
fireEvent.click(remove);
|
||||
await waitFor(() => expect(screen.queryByTitle('dash.example.com')).toBeNull());
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
await waitFor(() => expect(save).toHaveBeenCalledTimes(1));
|
||||
expect(save.mock.calls[0][0]).toMatchObject({ sni: '', snis: [] });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user