feat(panel): build-before-bind + socks5 fixed node account (items 20+21)
Item 20: drop the zero-client deploy gate in ValidateNodeDeploy for all protocols, stop RebuildNode from auto-removing a node whose bindings reach zero, and let server/heartbeat reconciliation rebuild zero-client nodes like any other. Nodes are built and deployed first; bindings can be added and removed freely. Item 21: socks5-auth nodes carry one node-level fixed account (node.socksUser/socksPassword, minted at creation or by the store-load migration MigrateSocksNodeCredentials, idempotent). The sing-box inbound's only user is that pair regardless of bindings; subscription links for every bound customer carry the same pair. The generated username avoids the ng: prefix so the agent never attributes the account's traffic to a customer — it stays on the node's inbound counters. Edit form shows both fields (copyable) for socks nodes; NormalizeNode validates operator-supplied usernames.
This commit is contained in:
+7
-2
@@ -904,6 +904,8 @@ function nodeForm(item = null) {
|
||||
<label>入口端口<div class="inline-fields"><select name="portMode" data-node-sync><option value="random"${selected('random', node.portMode)}>范围内随机</option><option value="fixed"${selected('fixed', node.portMode)}>固定端口</option></select><input name="port" type="number" value="${esc(node.port || '')}" placeholder="固定时填写" min="1024" max="65535"></div></label>
|
||||
<label data-reality-only>Reality 目标预设<select name="realityPreset" data-reality-preset>${state.realityPresets.map((entry) => `<option value="${esc(entry.id)}" data-sni="${esc(entry.serverName)}" data-port="${entry.destPort}"${selected(entry.id, preset.id)}>${esc(entry.label)} · ${esc(entry.serverName)}</option>`).join('')}<option value="custom"${selected('custom', preset.id)}>自定义</option></select></label>
|
||||
<label data-reality-only>Reality SNI / 目标端口<div class="inline-fields"><input name="realityServerName" value="${esc(node.realityServerName || 'www.tesla.com')}" placeholder="www.tesla.com"><input name="realityDestPort" type="number" value="${Number(node.realityDestPort || 443)}" min="1" max="65535"></div></label>
|
||||
<label data-socks-only>SOCKS5 固定用户名<div class="inline-fields"><input name="socksUser" value="${esc(node.socksUser || '')}" placeholder="留空自动生成"><button type="button" class="icon-btn" data-tip="复制" data-action="copy-field" data-field="socksUser">${ICONS.copy}</button></div></label>
|
||||
<label data-socks-only>SOCKS5 固定密码<div class="inline-fields"><input name="socksPassword" value="${esc(node.socksPassword || '')}" placeholder="留空自动生成"><button type="button" class="icon-btn" data-tip="复制" data-action="copy-field" data-field="socksPassword">${ICONS.copy}</button></div></label>
|
||||
<label>拓扑<select name="topology" data-node-sync><option value="forward"${selected('forward', node.topology || 'forward')}>转发(入口 → 出口)</option><option value="direct"${selected('direct', node.topology)}>单机直连(无需出口)</option></select></label>
|
||||
<div class="section-title" data-forward-only>出口与转发传输</div>
|
||||
<div class="wide" data-forward-only><div class="segctl" id="exit-mode-seg"><button type="button" data-action="exit-mode" data-mode="managed">受管设备</button><button type="button" data-action="exit-mode" data-mode="manual">手动落地端点</button></div></div>
|
||||
@@ -927,6 +929,8 @@ function syncNodeForm() {
|
||||
const reality = ['vless-reality-vision', 'vless-reality'].includes(form.elements.protocol.value);
|
||||
$$('[data-reality-only]', form).forEach((element) => element.classList.toggle('hidden', !reality));
|
||||
form.elements.realityServerName.required = reality;
|
||||
const socks = form.elements.protocol.value === 'socks5-auth';
|
||||
$$('[data-socks-only]', form).forEach((element) => element.classList.toggle('hidden', !socks));
|
||||
for (const [mode, port] of [['portMode', 'port'], ['exitPortMode', 'exitPort']]) {
|
||||
const fixed = form.elements[mode].value === 'fixed' && (mode === 'portMode' || !direct);
|
||||
form.elements[port].disabled = !fixed;
|
||||
@@ -1104,7 +1108,7 @@ document.addEventListener('submit', async (event) => {
|
||||
closeModal(); toast(resourceId ? '客户信息已更新' : '客户已创建'); await load('customers');
|
||||
} else if (form.id === 'node-form') {
|
||||
const resourceId = data.get('resourceId');
|
||||
const payload = { name: data.get('name'), serverId: data.get('serverId'), protocol: data.get('protocol'), portMode: data.get('portMode'), port: data.get('port') || null, networkMode: data.get('networkMode'), topology: data.get('topology'), exitServerId: data.get('exitServerId') || null, exitProtocol: data.get('exitProtocol') || null, exitPortMode: data.get('exitPortMode') || null, exitPort: data.get('exitPort') || null, realityServerName: data.get('realityServerName'), realityDestPort: Number(data.get('realityDestPort') || 443), exitMode: form.dataset.exitMode || 'managed', exitAddress: data.get('exitAddress') || null };
|
||||
const payload = { name: data.get('name'), serverId: data.get('serverId'), protocol: data.get('protocol'), portMode: data.get('portMode'), port: data.get('port') || null, networkMode: data.get('networkMode'), topology: data.get('topology'), exitServerId: data.get('exitServerId') || null, exitProtocol: data.get('exitProtocol') || null, exitPortMode: data.get('exitPortMode') || null, exitPort: data.get('exitPort') || null, realityServerName: data.get('realityServerName'), realityDestPort: Number(data.get('realityDestPort') || 443), exitMode: form.dataset.exitMode || 'managed', exitAddress: data.get('exitAddress') || null, socksUser: data.get('socksUser') || null, socksPassword: data.get('socksPassword') || null };
|
||||
if (payload.topology !== 'direct' && payload.exitMode === 'manual') {
|
||||
const creds = {};
|
||||
if (data.get('exitClientId')) creds.clientId = data.get('exitClientId');
|
||||
@@ -1114,7 +1118,7 @@ document.addEventListener('submit', async (event) => {
|
||||
payload.exitPortMode = 'fixed';
|
||||
}
|
||||
await api(resourceId ? `/api/nodes/${resourceId}` : '/api/nodes', { method: resourceId ? 'PATCH' : 'POST', body: JSON.stringify(payload) });
|
||||
closeModal(); toast(resourceId ? '节点已更新,运行中节点已重建下发' : '节点已创建,请分配客户后部署'); await load('chains');
|
||||
closeModal(); toast(resourceId ? '节点已更新,运行中节点已重建下发' : '节点已创建,可直接部署,客户可随时增减绑定'); await load('chains');
|
||||
} else if (form.id === 'account-form') {
|
||||
if (data.get('newPassword') !== data.get('confirmPassword')) throw new Error('两次输入的新密码不一致');
|
||||
await api('/api/account', { method: 'PATCH', body: JSON.stringify({ username: data.get('username'), currentPassword: data.get('currentPassword'), newPassword: data.get('newPassword') }) });
|
||||
@@ -1173,6 +1177,7 @@ document.addEventListener('click', async (event) => {
|
||||
const action = button.dataset.action; const itemId = button.dataset.id;
|
||||
try {
|
||||
if (action === 'copy-text') { await copyText(button.dataset.value); toast('已复制到剪贴板'); }
|
||||
else if (action === 'copy-field') { const input = button.form ? button.form.elements[button.dataset.field] : null; await copyText(input ? input.value : ''); toast('已复制到剪贴板'); }
|
||||
else if (action === 'add-server') serverForm();
|
||||
else if (action === 'edit-server') serverForm(serverById(itemId));
|
||||
else if (action === 'enroll-server') await enrollModal(itemId);
|
||||
|
||||
Reference in New Issue
Block a user