Files
3x-ui/frontend/src/pages/clients/BulkDetachInboundsModal.tsx
T
MHSanaei 6964d84742 feat(reality): add live REALITY target scanner with IP/CIDR discovery
Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate.

- Single-domain validate auto-fills target and serverNames from the cert SAN
- Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe
- New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries
- Add scanner strings to all 13 locales
- Replace deprecated AntD Alert message prop with title across the panel
2026-06-26 22:18:47 +02:00

108 lines
3.3 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Modal, Select, Typography, message } from 'antd';
import { SelectAllClearButtons } from '@/components/form';
import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { BulkDetachResult } from '@/schemas/client';
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
interface BulkDetachInboundsModalProps {
open: boolean;
count: number;
inbounds: InboundOption[];
onOpenChange: (open: boolean) => void;
onSubmit: (inboundIds: number[]) => Promise<BulkDetachResult | null>;
}
export default function BulkDetachInboundsModal({
open,
count,
inbounds,
onOpenChange,
onSubmit,
}: BulkDetachInboundsModalProps) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [targetIds, setTargetIds] = useState<number[]>([]);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (open) setTargetIds([]);
}, [open]);
const targetOptions = useMemo(() => {
return (inbounds || [])
.filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
.map((ib) => ({
value: ib.id,
label: formatInboundLabel(ib.tag, ib.remark),
}));
}, [inbounds]);
async function submit() {
if (targetIds.length === 0 || count === 0) return;
setSubmitting(true);
try {
const result = await onSubmit(targetIds);
if (!result) return;
const detached = result.detached?.length ?? 0;
const skipped = result.skipped?.length ?? 0;
const errors = result.errors?.length ?? 0;
if (errors > 0) {
messageApi.warning(
t('pages.clients.detachFromInboundsResultMixed', { detached, skipped, errors }),
);
} else {
messageApi.success(t('pages.clients.detachFromInboundsResult', { detached, skipped }));
}
onOpenChange(false);
} finally {
setSubmitting(false);
}
}
return (
<>
{messageContextHolder}
<Modal
open={open}
title={t('pages.clients.detachFromInboundsTitle', { count })}
okText={t('pages.clients.detach')}
cancelText={t('cancel')}
okButtonProps={{ danger: true, disabled: targetIds.length === 0, loading: submitting }}
onCancel={() => onOpenChange(false)}
onOk={submit}
destroyOnHidden
>
<Typography.Paragraph type="secondary">
{t('pages.clients.detachFromInboundsDesc', { count })}
</Typography.Paragraph>
{targetOptions.length === 0 ? (
<Alert type="info" showIcon title={t('pages.clients.detachFromInboundsNoTargets')} />
) : (
<>
<SelectAllClearButtons
options={targetOptions}
value={targetIds}
onChange={setTargetIds}
/>
<Select
mode="multiple"
style={{ width: '100%' }}
value={targetIds}
onChange={setTargetIds}
options={targetOptions}
placeholder={t('pages.clients.detachFromInboundsTargets')}
optionFilterProp="label"
autoFocus
/>
</>
)}
</Modal>
</>
);
}