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.
This commit is contained in:
MHSanaei 2026-05-27 22:00:30 +02:00
parent 5f9528862b
commit a9b8458bde
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
4 changed files with 33 additions and 0 deletions

View file

@ -26,6 +26,7 @@ import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client'; import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); 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([ const MULTI_CLIENT_PROTOCOLS = new Set([
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria',
@ -77,6 +78,7 @@ interface FormState {
password: string; password: string;
auth: string; auth: string;
flow: string; flow: string;
security: string;
reverseTag: string; reverseTag: string;
totalGB: number; totalGB: number;
expiryDate: Dayjs | null; expiryDate: Dayjs | null;
@ -99,6 +101,7 @@ function emptyForm(): FormState {
password: '', password: '',
auth: '', auth: '',
flow: '', flow: '',
security: 'auto',
reverseTag: '', reverseTag: '',
totalGB: 0, totalGB: 0,
expiryDate: null, expiryDate: null,
@ -163,6 +166,7 @@ export default function ClientFormModal({
password: client.password || '', password: client.password || '',
auth: client.auth || '', auth: client.auth || '',
flow: client.flow || '', flow: client.flow || '',
security: client.security || 'auto',
reverseTag: client.reverse?.tag || '', reverseTag: client.reverse?.tag || '',
totalGB: bytesToGB(client.totalGB || 0), totalGB: bytesToGB(client.totalGB || 0),
reset: Number(client.reset) || 0, reset: Number(client.reset) || 0,
@ -214,6 +218,14 @@ export default function ClientFormModal({
return ids; return ids;
}, [inbounds]); }, [inbounds]);
const vmessIds = useMemo(() => {
const ids = new Set<number>();
for (const row of inbounds || []) {
if (row && row.protocol === 'vmess') ids.add(row.id);
}
return ids;
}, [inbounds]);
const showFlow = useMemo( const showFlow = useMemo(
() => (form.inboundIds || []).some((id) => flowCapableIds.has(id)), () => (form.inboundIds || []).some((id) => flowCapableIds.has(id)),
[form.inboundIds, flowCapableIds], [form.inboundIds, flowCapableIds],
@ -224,6 +236,11 @@ export default function ClientFormModal({
[form.inboundIds, vlessLikeIds], [form.inboundIds, vlessLikeIds],
); );
const showSecurity = useMemo(
() => (form.inboundIds || []).some((id) => vmessIds.has(id)),
[form.inboundIds, vmessIds],
);
useEffect(() => { useEffect(() => {
if (!showFlow && form.flow) { if (!showFlow && form.flow) {
@ -286,6 +303,7 @@ export default function ClientFormModal({
password: form.password, password: form.password,
auth: form.auth, auth: form.auth,
flow: form.flow, flow: form.flow,
security: form.security,
reverseTag: form.reverseTag, reverseTag: form.reverseTag,
totalGB: form.totalGB, totalGB: form.totalGB,
delayedStart: form.delayedStart, delayedStart: form.delayedStart,
@ -313,6 +331,7 @@ export default function ClientFormModal({
password: form.password, password: form.password,
auth: form.auth, auth: form.auth,
flow: showFlow ? (form.flow || '') : '', flow: showFlow ? (form.flow || '') : '',
security: showSecurity ? (form.security || 'auto') : 'auto',
totalGB: gbToBytes(form.totalGB), totalGB: gbToBytes(form.totalGB),
expiryTime, expiryTime,
reset: Number(form.reset) || 0, reset: Number(form.reset) || 0,
@ -497,6 +516,17 @@ export default function ClientFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
)} )}
{showSecurity && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.vmessSecurity')}>
<Select
value={form.security}
onChange={(v) => update('security', v)}
options={VMESS_SECURITY_OPTIONS.map((k) => ({ value: k, label: k }))}
/>
</Form.Item>
</Col>
)}
</Row> </Row>
<Row gutter={16}> <Row gutter={16}>

View file

@ -113,6 +113,7 @@ export const ClientFormSchema = z.object({
password: z.string(), password: z.string(),
auth: z.string(), auth: z.string(),
flow: z.string(), flow: z.string(),
security: z.string(),
reverseTag: z.string(), reverseTag: z.string(),
totalGB: z.number().min(0), totalGB: z.number().min(0),
delayedStart: z.boolean(), delayedStart: z.boolean(),

View file

@ -545,6 +545,7 @@
"hysteriaAuth": "Hysteria Auth", "hysteriaAuth": "Hysteria Auth",
"uuid": "UUID", "uuid": "UUID",
"flow": "Flow", "flow": "Flow",
"vmessSecurity": "VMess Security",
"reverseTag": "Reverse tag", "reverseTag": "Reverse tag",
"reverseTagPlaceholder": "Optional reverse tag", "reverseTagPlaceholder": "Optional reverse tag",
"telegramId": "Telegram user ID", "telegramId": "Telegram user ID",

View file

@ -509,6 +509,7 @@
"hysteriaAuth": "Auth (هیستریا)", "hysteriaAuth": "Auth (هیستریا)",
"uuid": "UUID", "uuid": "UUID",
"flow": "Flow", "flow": "Flow",
"vmessSecurity": "امنیت VMess",
"reverseTag": "Reverse tag", "reverseTag": "Reverse tag",
"reverseTagPlaceholder": "Reverse tag اختیاری", "reverseTagPlaceholder": "Reverse tag اختیاری",
"telegramId": "شناسه کاربر تلگرام", "telegramId": "شناسه کاربر تلگرام",