diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 6f609f89b..c071a9fe1 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -5476,7 +5476,7 @@ "tags": [ "Clients" ], - "summary": "Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. Returns the adjusted count and per-email skip reasons.", + "summary": "Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. The optional flow directive sets the XTLS flow on every client: \"none\" clears it, \"xtls-rprx-vision\"/\"xtls-rprx-vision-udp443\" set it where the inbound supports it (omit or \"\" to leave it unchanged). Returns the adjusted count and per-email skip reasons.", "operationId": "post_panel_api_clients_bulkAdjust", "requestBody": { "required": true, @@ -5491,7 +5491,8 @@ "bob" ], "addDays": 30, - "addBytes": 53687091200 + "addBytes": 53687091200, + "flow": "xtls-rprx-vision" } } } diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 891a9c3aa..92c8077a1 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -341,7 +341,7 @@ export function useClients() { }); const bulkAdjustMut = useMutation({ - mutationFn: async (payload: { emails: string[]; addDays: number; addBytes: number }): Promise> => { + mutationFn: async (payload: { emails: string[]; addDays: number; addBytes: number; flow: string }): Promise> => { const raw = await HttpUtil.post('/panel/api/clients/bulkAdjust', payload, JSON_HEADERS); return parseMsg(raw, BulkAdjustResultSchema, 'clients/bulkAdjust'); }, @@ -435,9 +435,9 @@ export function useClients() { if (!Array.isArray(payloads) || payloads.length === 0) return Promise.resolve(null as unknown as Msg); return bulkCreateMut.mutateAsync(payloads); }, [bulkCreateMut]); - const bulkAdjust = useCallback((emails: string[], addDays: number, addBytes: number) => { + const bulkAdjust = useCallback((emails: string[], addDays: number, addBytes: number, flow = '') => { if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null); - return bulkAdjustMut.mutateAsync({ emails, addDays, addBytes }); + return bulkAdjustMut.mutateAsync({ emails, addDays, addBytes, flow }); }, [bulkAdjustMut]); const bulkAddToGroup = useCallback((emails: string[], group: string) => { if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve(null); diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index fb7168f43..960d5dd20 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -635,8 +635,8 @@ export const sections: readonly Section[] = [ { method: 'POST', path: '/panel/api/clients/bulkAdjust', - summary: 'Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. Returns the adjusted count and per-email skip reasons.', - body: '{\n "emails": ["alice", "bob"],\n "addDays": 30,\n "addBytes": 53687091200\n}', + summary: 'Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. The optional flow directive sets the XTLS flow on every client: "none" clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the inbound supports it (omit or "" to leave it unchanged). Returns the adjusted count and per-email skip reasons.', + body: '{\n "emails": ["alice", "bob"],\n "addDays": 30,\n "addBytes": 53687091200,\n "flow": "xtls-rprx-vision"\n}', response: '{\n "success": true,\n "obj": {\n "adjusted": 2,\n "skipped": [\n { "email": "carol", "reason": "unlimited expiry" }\n ]\n }\n}', }, { diff --git a/frontend/src/pages/clients/ClientBulkAdjustModal.tsx b/frontend/src/pages/clients/ClientBulkAdjustModal.tsx index 5fbab5a30..897a1a940 100644 --- a/frontend/src/pages/clients/ClientBulkAdjustModal.tsx +++ b/frontend/src/pages/clients/ClientBulkAdjustModal.tsx @@ -1,16 +1,19 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, Form, InputNumber, Modal, message } from 'antd'; +import { Alert, Form, InputNumber, Modal, Select, message } from 'antd'; import { ClientBulkAdjustFormSchema } from '@/schemas/client'; +import { TLS_FLOW_CONTROL } from '@/schemas/primitives/flow'; const GB = 1024 * 1024 * 1024; +const FLOW_CLEAR = 'none'; + interface ClientBulkAdjustModalProps { open: boolean; count: number; onOpenChange: (open: boolean) => void; - onSubmit: (addDays: number, addBytes: number) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>; + onSubmit: (addDays: number, addBytes: number, flow: string) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>; } export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSubmit }: ClientBulkAdjustModalProps) { @@ -18,12 +21,14 @@ export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSub const [messageApi, messageContextHolder] = message.useMessage(); const [addDays, setAddDays] = useState(0); const [addGB, setAddGB] = useState(0); + const [flow, setFlow] = useState(''); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (open) { setAddDays(0); setAddGB(0); + setFlow(''); } }, [open]); @@ -31,16 +36,17 @@ export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSub const validated = ClientBulkAdjustFormSchema.safeParse({ addDays: Math.trunc(Number(addDays) || 0), addGB: Number(addGB) || 0, + flow, }); if (!validated.success) { messageApi.warning(t(validated.error.issues[0]?.message ?? 'somethingWentWrong')); return; } - const { addDays: days, addGB: gb } = validated.data; + const { addDays: days, addGB: gb, flow: flowValue } = validated.data; setSubmitting(true); try { const bytes = Math.trunc(gb * GB); - const result = await onSubmit(days, bytes); + const result = await onSubmit(days, bytes, flowValue); if (!result) return; const ok = result.adjusted ?? 0; const skipped = result.skipped?.length ?? 0; @@ -95,6 +101,18 @@ export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSub step={1} /> + +