From a9b8458bde722d5bd38b85214795a0c01fc4cf34 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 22:00:30 +0200 Subject: [PATCH] feat(clients): per-client VMess security in client form Restores the VMess `security` selector on the client form (auto, aes-128-gcm, chacha20-poly1305, none, zero) and surfaces it only when at least one attached inbound is VMess. The value rides into the share link via the existing `scy=` field in genVmessLink; the panel persists it on ClientRecord and in the inbound's settings.clients so the link generator can read it back. Adds the pages.clients.vmessSecurity i18n key in en-US and fa-IR. --- .../src/pages/clients/ClientFormModal.tsx | 30 +++++++++++++++++++ frontend/src/schemas/client.ts | 1 + web/translation/en-US.json | 1 + web/translation/fa-IR.json | 1 + 4 files changed, 33 insertions(+) diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index c2f83de3..190048f5 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -26,6 +26,7 @@ import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); +const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', 'zero'] as const; const MULTI_CLIENT_PROTOCOLS = new Set([ 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', @@ -77,6 +78,7 @@ interface FormState { password: string; auth: string; flow: string; + security: string; reverseTag: string; totalGB: number; expiryDate: Dayjs | null; @@ -99,6 +101,7 @@ function emptyForm(): FormState { password: '', auth: '', flow: '', + security: 'auto', reverseTag: '', totalGB: 0, expiryDate: null, @@ -163,6 +166,7 @@ export default function ClientFormModal({ password: client.password || '', auth: client.auth || '', flow: client.flow || '', + security: client.security || 'auto', reverseTag: client.reverse?.tag || '', totalGB: bytesToGB(client.totalGB || 0), reset: Number(client.reset) || 0, @@ -214,6 +218,14 @@ export default function ClientFormModal({ return ids; }, [inbounds]); + const vmessIds = useMemo(() => { + const ids = new Set(); + for (const row of inbounds || []) { + if (row && row.protocol === 'vmess') ids.add(row.id); + } + return ids; + }, [inbounds]); + const showFlow = useMemo( () => (form.inboundIds || []).some((id) => flowCapableIds.has(id)), [form.inboundIds, flowCapableIds], @@ -224,6 +236,11 @@ export default function ClientFormModal({ [form.inboundIds, vlessLikeIds], ); + const showSecurity = useMemo( + () => (form.inboundIds || []).some((id) => vmessIds.has(id)), + [form.inboundIds, vmessIds], + ); + useEffect(() => { if (!showFlow && form.flow) { @@ -286,6 +303,7 @@ export default function ClientFormModal({ password: form.password, auth: form.auth, flow: form.flow, + security: form.security, reverseTag: form.reverseTag, totalGB: form.totalGB, delayedStart: form.delayedStart, @@ -313,6 +331,7 @@ export default function ClientFormModal({ password: form.password, auth: form.auth, flow: showFlow ? (form.flow || '') : '', + security: showSecurity ? (form.security || 'auto') : 'auto', totalGB: gbToBytes(form.totalGB), expiryTime, reset: Number(form.reset) || 0, @@ -497,6 +516,17 @@ export default function ClientFormModal({ )} + {showSecurity && ( + + +