mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
feat(clients,inbound): Auto Renew in Bulk Add + cleaner inbound wire payload
Bulk Add now exposes the same Auto Renew (`reset`, days) input as the single-client form, applied to every client the batch produces. The field was already on ClientBulkAddFormSchema's siblings; just wire it into the schema, the empty-form defaults, the UI, and the bulkCreate payload. Also relabel "Subscription info" to "Subscription ID" by switching to the canonical pages.clients.subId key and modernise the SyncOutlined-in-label random affordance on the same row. On the inbound submit path, two payload-shape cleanups in dropLegacyOptionalEmpties: - streamSettings.hysteriaSettings.auth is a holdover slot whose real per-client value lives in settings.clients[*].auth; drop the field entirely when empty instead of shipping `"auth": ""`. - finalmask's `tcp` / `udp` arrays were already dropped together when both were empty, but a UDP-only setup still emitted a stray `"tcp": []`. Drop each sub-array on its own when empty so a Hysteria-style "salamander on udp only" config no longer carries the empty tcp sibling.
This commit is contained in:
parent
43288e6686
commit
f1e433e839
3 changed files with 46 additions and 13 deletions
|
|
@ -227,15 +227,32 @@ export function dropLegacyOptionalEmpties(
|
||||||
const fb = settings.fallbacks;
|
const fb = settings.fallbacks;
|
||||||
if (Array.isArray(fb) && fb.length === 0) delete settings.fallbacks;
|
if (Array.isArray(fb) && fb.length === 0) delete settings.fallbacks;
|
||||||
|
|
||||||
// StreamSettings emits `finalmask` only when at least one transport
|
|
||||||
// mask exists (legacy `hasFinalMask`). Otherwise drop the whole block.
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
// StreamSettings emits `finalmask` only when at least one transport
|
||||||
|
// mask exists (legacy `hasFinalMask`). Drop the whole block when all
|
||||||
|
// sub-fields are empty; otherwise drop only the empty sub-arrays so
|
||||||
|
// the wire payload doesn't carry a stray `"tcp": []` next to a
|
||||||
|
// populated UDP mask list (and vice versa).
|
||||||
const fm = stream.finalmask as { tcp?: unknown[]; udp?: unknown[]; quicParams?: unknown } | undefined;
|
const fm = stream.finalmask as { tcp?: unknown[]; udp?: unknown[]; quicParams?: unknown } | undefined;
|
||||||
if (fm && typeof fm === 'object') {
|
if (fm && typeof fm === 'object') {
|
||||||
const hasTcp = Array.isArray(fm.tcp) && fm.tcp.length > 0;
|
const hasTcp = Array.isArray(fm.tcp) && fm.tcp.length > 0;
|
||||||
const hasUdp = Array.isArray(fm.udp) && fm.udp.length > 0;
|
const hasUdp = Array.isArray(fm.udp) && fm.udp.length > 0;
|
||||||
const hasQuic = fm.quicParams != null;
|
const hasQuic = fm.quicParams != null;
|
||||||
if (!hasTcp && !hasUdp && !hasQuic) delete stream.finalmask;
|
if (!hasTcp && !hasUdp && !hasQuic) {
|
||||||
|
delete stream.finalmask;
|
||||||
|
} else {
|
||||||
|
if (!hasTcp) delete fm.tcp;
|
||||||
|
if (!hasUdp) delete fm.udp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hysteria's per-client auth lives in settings.clients[*].auth; the
|
||||||
|
// streamSettings.hysteriaSettings.auth slot is a holdover from older
|
||||||
|
// hysteria builds and serves no purpose on the inbound side, so an
|
||||||
|
// empty value shouldn't ride along in the JSON payload.
|
||||||
|
const hs = stream.hysteriaSettings as { auth?: string } | undefined;
|
||||||
|
if (hs && typeof hs === 'object' && (hs.auth === '' || hs.auth == null)) {
|
||||||
|
delete hs.auth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd';
|
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, message } from 'antd';
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
|
@ -41,6 +41,7 @@ function emptyForm(): FormState {
|
||||||
limitIp: 0,
|
limitIp: 0,
|
||||||
totalGB: 0,
|
totalGB: 0,
|
||||||
expiryTime: 0,
|
expiryTime: 0,
|
||||||
|
reset: 0,
|
||||||
inboundIds: [],
|
inboundIds: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +155,7 @@ export default function ClientBulkAddModal({
|
||||||
flow: showFlow ? (form.flow || '') : '',
|
flow: showFlow ? (form.flow || '') : '',
|
||||||
totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB),
|
totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB),
|
||||||
expiryTime: form.expiryTime,
|
expiryTime: form.expiryTime,
|
||||||
|
reset: Number(form.reset) || 0,
|
||||||
limitIp: Number(form.limitIp) || 0,
|
limitIp: Number(form.limitIp) || 0,
|
||||||
comment: form.comment,
|
comment: form.comment,
|
||||||
enable: true,
|
enable: true,
|
||||||
|
|
@ -247,16 +249,18 @@ export default function ClientBulkAddModal({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label={
|
<Form.Item label={t('pages.clients.subId')}>
|
||||||
<>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
{t('subscription.title')}
|
<Input
|
||||||
<SyncOutlined
|
value={form.subId}
|
||||||
className="random-icon"
|
onChange={(e) => update('subId', e.target.value)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
|
onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
|
||||||
/>
|
/>
|
||||||
</>
|
</Space.Compact>
|
||||||
}>
|
|
||||||
<Input value={form.subId} onChange={(e) => update('subId', e.target.value)} />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('comment')}>
|
<Form.Item label={t('comment')}>
|
||||||
|
|
@ -310,6 +314,17 @@ export default function ClientBulkAddModal({
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.clients.renew')}
|
||||||
|
tooltip={t('pages.clients.renewDesc')}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
value={form.reset}
|
||||||
|
min={0}
|
||||||
|
onChange={(v) => update('reset', Number(v) || 0)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ export const ClientBulkAddFormSchema = z.object({
|
||||||
limitIp: z.number().int().min(0),
|
limitIp: z.number().int().min(0),
|
||||||
totalGB: z.number().min(0),
|
totalGB: z.number().min(0),
|
||||||
expiryTime: z.number(),
|
expiryTime: z.number(),
|
||||||
|
reset: z.number().int().min(0),
|
||||||
inboundIds: z.array(z.number()).min(1, 'pages.clients.selectInbound'),
|
inboundIds: z.array(z.number()).min(1, 'pages.clients.selectInbound'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue