diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts index 4c78fcc8..1c8e2aa5 100644 --- a/frontend/src/lib/xray/inbound-defaults.ts +++ b/frontend/src/lib/xray/inbound-defaults.ts @@ -185,13 +185,16 @@ export function createDefaultHysteriaInboundSettings( } export function createDefaultHttpInboundSettings(): HttpInboundSettings { - return { accounts: [], allowTransparent: false }; + return { + accounts: [{ user: RandomUtil.randomLowerAndNum(8), pass: RandomUtil.randomLowerAndNum(12) }], + allowTransparent: false, + }; } export function createDefaultMixedInboundSettings(): MixedInboundSettings { return { auth: 'password', - accounts: [], + accounts: [{ user: RandomUtil.randomLowerAndNum(8), pass: RandomUtil.randomLowerAndNum(12) }], udp: false, ip: '127.0.0.1', }; diff --git a/frontend/src/pages/inbounds/AssignClientsGroupModal.tsx b/frontend/src/pages/inbounds/AssignClientsGroupModal.tsx new file mode 100644 index 00000000..0c4bc208 --- /dev/null +++ b/frontend/src/pages/inbounds/AssignClientsGroupModal.tsx @@ -0,0 +1,61 @@ +import { lazy, useEffect, useMemo, useState } from 'react'; + +import { HttpUtil } from '@/utils'; +import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound'; + +const BulkAssignGroupModal = lazy(() => import('@/pages/clients/BulkAssignGroupModal')); + +interface AssignClientsGroupModalProps { + open: boolean; + source: DBInbound | null; + onClose: () => void; + onAssigned?: () => void; +} + +function readClientEmails(settings: unknown): string[] { + const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> }; + const clients = Array.isArray(parsed?.clients) ? parsed.clients : []; + return clients.map((c) => (c?.email || '').trim()).filter(Boolean); +} + +export default function AssignClientsGroupModal({ + open, + source, + onClose, + onAssigned, +}: AssignClientsGroupModalProps) { + const [groups, setGroups] = useState([]); + + const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]); + + useEffect(() => { + if (!open) return; + let cancelled = false; + (async () => { + const msg = await HttpUtil.get('/panel/api/clients/groups', undefined, { silent: true }); + if (cancelled) return; + const list = Array.isArray(msg?.obj) ? (msg.obj as Array<{ name?: string }>) : []; + setGroups(list.map((g) => g?.name || '').filter(Boolean)); + })(); + return () => { cancelled = true; }; + }, [open]); + + return ( + { if (!o) onClose(); }} + onSubmit={async (group) => { + const msg = await HttpUtil.post( + '/panel/api/clients/bulkAssignGroup', + { emails, group }, + { headers: { 'Content-Type': 'application/json' } }, + ); + if (!msg?.success) return null; + onAssigned?.(); + return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 }; + }} + /> + ); +} diff --git a/frontend/src/pages/inbounds/AttachClientsModal.tsx b/frontend/src/pages/inbounds/AttachClientsModal.tsx new file mode 100644 index 00000000..53b16df1 --- /dev/null +++ b/frontend/src/pages/inbounds/AttachClientsModal.tsx @@ -0,0 +1,108 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert, Modal, Select, Typography, message } from 'antd'; + +import { HttpUtil } from '@/utils'; +import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound'; +import { isInboundMultiUser } from './InboundList'; + +interface AttachClientsModalProps { + open: boolean; + source: DBInbound | null; + dbInbounds: DBInbound[]; + onClose: () => void; + onAttached?: () => void; +} + +interface BulkAttachResult { + attached?: string[]; + skipped?: string[]; + errors?: string[]; +} + +function readClientEmails(settings: unknown): string[] { + const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> }; + const clients = Array.isArray(parsed?.clients) ? parsed.clients : []; + return clients.map((c) => (c?.email || '').trim()).filter(Boolean); +} + +export default function AttachClientsModal({ + open, + source, + dbInbounds, + onClose, + onAttached, +}: AttachClientsModalProps) { + const { t } = useTranslation(); + const [messageApi, messageContextHolder] = message.useMessage(); + const [targetIds, setTargetIds] = useState([]); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (open) setTargetIds([]); + }, [open]); + + const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]); + + const targetOptions = useMemo(() => { + if (!source) return []; + return (dbInbounds || []) + .filter((ib) => ib.id !== source.id && isInboundMultiUser(ib)) + .map((ib) => ({ value: ib.id, label: `${ib.remark} (${ib.protocol}@${ib.port})` })); + }, [dbInbounds, source]); + + async function submit() { + if (!source || targetIds.length === 0 || emails.length === 0) return; + setSaving(true); + try { + const msg = await HttpUtil.post('/panel/api/clients/bulkAttach', { emails, inboundIds: targetIds }, { headers: { 'Content-Type': 'application/json' } }); + if (!msg?.success) { + messageApi.error(msg?.msg || t('somethingWentWrong')); + return; + } + const result = (msg.obj || {}) as BulkAttachResult; + const attached = result.attached?.length ?? 0; + const skipped = result.skipped?.length ?? 0; + const errors = result.errors?.length ?? 0; + if (errors > 0) { + messageApi.warning(t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors })); + } else { + messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped })); + } + onAttached?.(); + onClose(); + } finally { + setSaving(false); + } + } + + return ( + + {messageContextHolder} + + {t('pages.inbounds.attachClientsDesc', { count: emails.length })} + + {targetOptions.length === 0 ? ( + + ) : ( +