3x-ui/frontend/src/pages/inbounds/form/useInboundFallbacks.ts
MHSanaei 5af07dc562
refactor(frontend): slim InboundFormModal by extracting hooks + sections
Pull the modal's non-layout logic into focused files at the form root:
- useSecurityActions.ts: TLS/Reality key + cert generation handlers and
  onSecurityChange (consumed by the security tab)
- useInboundFallbacks.ts: fallback row state + load/save/derive/add/
  update/remove/move handlers + eligible-child options
- FallbacksCard.tsx: the fallbacks card UI (presentational)
- SniffingTab.tsx: the sniffing tab UI (presentational)

Also drop the stale "Pattern A rewrite / sibling file" header comment and
the imports the extractions made unused. InboundFormModal goes from 1332
to 868 lines with no behavior change (351 tests green, snapshots
unchanged).
2026-05-30 21:36:55 +02:00

187 lines
6.7 KiB
TypeScript

import { useRef, useState } from 'react';
import { HttpUtil } from '@/utils';
import type { FallbackRow } from '@/schemas/forms/inbound-form';
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
// Fallback rows for VLESS/Trojan TLS inbounds: state + the load/save/derive
// and add/update/remove/move handlers, plus the eligible-child option list.
// Lifted out of InboundFormModal so the modal body stays focused on layout.
export function useInboundFallbacks(dbInbound: DBInbound | null, dbInbounds: DBInbound[]) {
const fallbackKeyRef = useRef(0);
const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
const fallbackChildOptions = (dbInbounds || [])
.filter((ib) => ib.id !== dbInbound?.id)
.map((ib) => ({
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
value: ib.id,
}));
const loadFallbacks = async (masterId: number | null) => {
if (!masterId) {
setFallbacks([]);
return;
}
const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
if (!msg?.success || !Array.isArray(msg.obj)) {
setFallbacks([]);
return;
}
setFallbacks(
(msg.obj as {
childId: number;
name?: string;
alpn?: string;
path?: string;
dest?: string;
xver?: number;
}[])
.map((r) => ({
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: r.childId,
name: r.name || '',
alpn: r.alpn || '',
path: r.path || '',
dest: r.dest || '',
xver: r.xver || 0,
})),
);
};
const saveFallbacks = async (masterId: number) => {
if (!masterId) return true;
const payload = {
fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({
childId: c.childId,
name: c.name,
alpn: c.alpn,
path: c.path,
dest: c.dest,
xver: Number(c.xver) || 0,
sortOrder: i,
})),
};
const msg = await HttpUtil.post(
`/panel/api/inbounds/${masterId}/fallbacks`,
payload,
{ headers: { 'Content-Type': 'application/json' } },
);
return !!msg?.success;
};
// Derive a fallback row's SNI / ALPN / Path / xver from a child
// inbound's streamSettings — what the legacy panel auto-filled when an
// operator wired a fallback target. SNI/ALPN come straight off the
// child's TLS block; path depends on the child's transport (ws/grpc
// /httpupgrade carry an explicit path; tcp/kcp/xhttp have no path of
// their own). xver stays 0 unless the child explicitly opts in via
// PROXY-protocol sockopt.
const deriveFallbackDefaults = (childId: number): Partial<FallbackRow> => {
const child = (dbInbounds || []).find((ib) => ib.id === childId);
if (!child) return {};
const stream = coerceInboundJsonField(child.streamSettings);
const tls = (stream.tlsSettings as Record<string, unknown> | undefined) ?? {};
const network = typeof stream.network === 'string' ? stream.network : '';
const sni = typeof tls.serverName === 'string' ? tls.serverName : '';
const alpnArr = Array.isArray(tls.alpn) ? tls.alpn : [];
const alpn = alpnArr.filter((v) => typeof v === 'string').join(',');
let path = '';
if (network === 'ws') {
const ws = (stream.wsSettings as Record<string, unknown> | undefined) ?? {};
if (typeof ws.path === 'string') path = ws.path;
} else if (network === 'grpc') {
const grpc = (stream.grpcSettings as Record<string, unknown> | undefined) ?? {};
if (typeof grpc.serviceName === 'string') path = grpc.serviceName;
} else if (network === 'httpupgrade') {
const hu = (stream.httpupgradeSettings as Record<string, unknown> | undefined) ?? {};
if (typeof hu.path === 'string') path = hu.path;
} else if (network === 'xhttp') {
const xh = (stream.xhttpSettings as Record<string, unknown> | undefined) ?? {};
if (typeof xh.path === 'string') path = xh.path;
}
return { name: sni, alpn, path, xver: 0 };
};
const addFallback = () => {
setFallbacks((prev) => [...prev, {
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: null,
name: '',
alpn: '',
path: '',
dest: '',
xver: 0,
}]);
};
const updateFallback = (rowKey: string, patch: Partial<FallbackRow>) => {
setFallbacks((prev) => prev.map((r) => {
if (r.rowKey !== rowKey) return r;
// When the picker selects a new child inbound and the row hasn't
// been hand-edited yet (sni/alpn/path/dest all blank, xver = 0),
// pull the SNI/ALPN/Path defaults off that child. Operators who
// intentionally typed values keep them — we only fill the empties.
if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0;
if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
}
return { ...r, ...patch };
}));
};
const removeFallback = (idx: number) => {
setFallbacks((prev) => prev.filter((_, i) => i !== idx));
};
// Move a fallback row up/down by swapping adjacent indices. The order
// is persisted via the fallback row's sortOrder (rebuilt by index on
// save), so reordering survives reloads.
const moveFallback = (idx: number, direction: -1 | 1) => {
setFallbacks((prev) => {
const target = idx + direction;
if (target < 0 || target >= prev.length) return prev;
const next = prev.slice();
[next[idx], next[target]] = [next[target], next[idx]];
return next;
});
};
// One-shot: add a fresh fallback row for every eligible inbound (i.e.
// every option in fallbackChildOptions) that is not already wired up.
// Convenient for operators who want catch-all routing to every host
// they manage on the panel.
const addAllFallbacks = () => {
setFallbacks((prev) => {
const alreadyHave = new Set(prev.map((r) => r.childId));
const additions = fallbackChildOptions
.filter((opt) => !alreadyHave.has(opt.value))
.map<FallbackRow>((opt) => {
const derived = deriveFallbackDefaults(opt.value);
return {
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: opt.value,
name: derived.name ?? '',
alpn: derived.alpn ?? '',
path: derived.path ?? '',
dest: '',
xver: derived.xver ?? 0,
};
});
if (additions.length === 0) return prev;
return [...prev, ...additions];
});
};
return {
fallbacks,
fallbackChildOptions,
loadFallbacks,
saveFallbacks,
addFallback,
updateFallback,
removeFallback,
moveFallback,
addAllFallbacks,
};
}