node-red-contrib-fox-admin 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ Node-RED 运行时插件,提供系统管理控制台功能。
24
24
  ## 使用方法
25
25
 
26
26
  1. 启动 Node-RED
27
- 2. 访问 `http://your-node-red-ip:port/foxcontroladmin_login/`
27
+ 2. 访问 `http://your-node-red-ip:port/foxadmin/`
28
28
  3. 使用默认密码登录:`foxcontrol@123`
29
29
  4. 修改密码:设置环境变量 `FOX_ADMIN_PASSWORD`
30
30
 
@@ -36,27 +36,27 @@ Node-RED 运行时插件,提供系统管理控制台功能。
36
36
  ## API 接口
37
37
 
38
38
  ### 认证相关
39
- - `POST /api/foxcontrol_admin/login`:登录
40
- - `POST /api/foxcontrol_admin/logout`:登出
41
- - `GET /api/foxcontrol_admin/check-auth`:检查认证状态
39
+ - `POST /foxcontrol_api/admin/login`:登录
40
+ - `POST /foxcontrol_api/admin/logout`:登出
41
+ - `GET /foxcontrol_api/admin/check-auth`:检查认证状态
42
42
 
43
43
  ### 系统管理
44
- - `GET /api/foxcontrol_admin/system/info`:获取系统信息
45
- - `PUT /api/foxcontrol_admin/hostname`:修改主机名
44
+ - `GET /foxcontrol_api/admin/system/info`:获取系统信息
45
+ - `PUT /foxcontrol_api/admin/hostname`:修改主机名
46
46
 
47
47
  ### 网络管理
48
- - `GET /api/foxcontrol_admin/network/interfaces`:获取网络接口
49
- - `GET /api/foxcontrol_admin/network/connections`:获取网络连接
50
- - `PUT /api/foxcontrol_admin/network/config`:配置网络
48
+ - `GET /foxcontrol_api/admin/network/interfaces`:获取网络接口
49
+ - `GET /foxcontrol_api/admin/network/connections`:获取网络连接
50
+ - `PUT /foxcontrol_api/admin/network/config`:配置网络
51
51
 
52
52
  ### 防火墙管理
53
- - `GET /api/foxcontrol_admin/firewall/status`:获取防火墙状态
54
- - `PUT /api/foxcontrol_admin/firewall/enable`:启用防火墙
55
- - `PUT /api/foxcontrol_admin/firewall/disable`:禁用防火墙
56
- - `POST /api/foxcontrol_admin/firewall/rule`:添加防火墙规则
53
+ - `GET /foxcontrol_api/admin/firewall/status`:获取防火墙状态
54
+ - `PUT /foxcontrol_api/admin/firewall/enable`:启用防火墙
55
+ - `PUT /foxcontrol_api/admin/firewall/disable`:禁用防火墙
56
+ - `POST /foxcontrol_api/admin/firewall/rule`:添加防火墙规则
57
57
 
58
58
  ### 站点部署
59
- - `POST /api/foxcontrol_admin/deploy/upload`:上传并部署站点
59
+ - `POST /foxcontrol_api/admin/deploy/upload`:上传并部署站点
60
60
 
61
61
  ## 注意事项
62
62
 
package/index.js CHANGED
@@ -234,20 +234,68 @@ module.exports = function(RED) {
234
234
  }
235
235
  });
236
236
 
237
+ async function getSubnetMask(ifaceName, targetIp) {
238
+ try {
239
+ const osType = getSystemType();
240
+ const platform = process.platform;
241
+ let cmd = '';
242
+
243
+ if (osType === 'debian' || osType === 'kylin' || osType === 'openEuler' || osType === 'ubuntu') {
244
+ if (targetIp) {
245
+ cmd = `ip -4 addr show dev ${ifaceName} 2>/dev/null | grep "inet ${targetIp}/" | awk '{print $2}' | cut -d'/' -f2`;
246
+ } else {
247
+ cmd = `ip -4 addr show dev ${ifaceName} 2>/dev/null | grep 'inet ' | awk 'NR==1 {print $2}' | cut -d'/' -f2`;
248
+ }
249
+ } else if (osType === 'unknown') {
250
+ if (platform === 'win32') {
251
+ cmd = `ipconfig | findstr "${ifaceName}"`;
252
+ } else {
253
+ return '';
254
+ }
255
+ } else {
256
+ return '';
257
+ }
258
+
259
+ const result = await execCommand(cmd);
260
+ if (platform === 'win32') {
261
+ const match = result.match(/Subnet Mask\s+:\s+(\d+\.\d+\.\d+\.\d+)/);
262
+ return match ? match[1] : '';
263
+ }
264
+ const cidr = result.trim();
265
+ if (cidr && /^\d+$/.test(cidr)) {
266
+ return cidrToDotted(cidr);
267
+ }
268
+ return '';
269
+ } catch (err) {
270
+ return '';
271
+ }
272
+ }
273
+
274
+ function cidrToDotted(cidr) {
275
+ const mask = ~0 << (32 - cidr);
276
+ return [
277
+ (mask >>> 24) & 255,
278
+ (mask >>> 16) & 255,
279
+ (mask >>> 8) & 255,
280
+ mask & 255
281
+ ].join('.');
282
+ }
283
+
237
284
  app.get('/foxcontrol_api/admin/network/interfaces', async (req, res) => {
238
285
  try {
239
286
  const networkInterfaces = await si.networkInterfaces();
240
287
  const networkStats = await si.networkStats();
241
288
  const defaultGateway = await getDefaultGateway();
242
289
 
243
- const interfaces = networkInterfaces.map(iface => {
290
+ const interfaces = await Promise.all(networkInterfaces.map(async (iface) => {
244
291
  const stats = networkStats.find(s => s.iface === iface.iface) || {};
292
+ const subnet = iface.ip4_subnet || await getSubnetMask(iface.iface, iface.ip4);
245
293
 
246
294
  return {
247
295
  iface: iface.iface,
248
296
  type: iface.type,
249
297
  ip4: iface.ip4,
250
- ip4_subnet: iface.ip4_subnet || '',
298
+ ip4_subnet: subnet,
251
299
  ip6: iface.ip6,
252
300
  mac: iface.mac,
253
301
  internal: iface.internal,
@@ -262,7 +310,7 @@ module.exports = function(RED) {
262
310
  rx_sec: stats.rx_sec || 0,
263
311
  tx_sec: stats.tx_sec || 0
264
312
  };
265
- });
313
+ }));
266
314
 
267
315
  res.json({ status: 'success', data: interfaces });
268
316
  } catch (err) {
@@ -303,7 +351,14 @@ iface ${iface} inet static
303
351
  fs.writeFileSync(configPath, config);
304
352
  await execCommand('systemctl restart networking');
305
353
  } else if (systemType === 'kylin' || systemType === 'openEuler') {
306
- const config = `
354
+ try {
355
+ await execCommand('systemctl restart network');
356
+ } catch (err) {
357
+ try {
358
+ await execCommand(`nmcli con modify ${iface} ipv4.addresses ${ip4}/${netmask} ipv4.gateway ${gateway} ipv4.dns ${dns} ipv4.method manual`);
359
+ await execCommand(`nmcli con up ${iface}`);
360
+ } catch (nmcliErr) {
361
+ const config = `
307
362
  TYPE=Ethernet
308
363
  BOOTPROTO=static
309
364
  DEFROUTE=yes
@@ -320,9 +375,12 @@ NETMASK=${netmask}
320
375
  GATEWAY=${gateway}
321
376
  DNS1=${dns}
322
377
  `;
323
- const configPath = `/etc/sysconfig/network-scripts/ifcfg-${iface}`;
324
- fs.writeFileSync(configPath, config);
325
- await execCommand('systemctl restart network');
378
+ const configPath = `/etc/sysconfig/network-scripts/ifcfg-${iface}`;
379
+ fs.writeFileSync(configPath, config);
380
+ await execCommand('nmcli con reload');
381
+ await execCommand(`nmcli con up ${iface}`);
382
+ }
383
+ }
326
384
  }
327
385
 
328
386
  RED.log.info(`[Fox Admin] 网络配置已更新: ${iface}`);
@@ -343,7 +401,18 @@ DNS1=${dns}
343
401
  const ufwStatus = await execCommand('ufw status verbose');
344
402
  status.enabled = ufwStatus.includes('Status: active');
345
403
  const rules = ufwStatus.split('\n').filter(line => line.includes('ALLOW') || line.includes('DENY'));
346
- status.rules = rules;
404
+ status.rules = rules.map(rule => {
405
+ const match = rule.match(/(\d+)\/(tcp|udp)/);
406
+ if (match) {
407
+ return {
408
+ action: rule.includes('ALLOW') ? 'allow' : 'deny',
409
+ port: match[1],
410
+ protocol: match[2],
411
+ ip: rule.includes('from') ? rule.match(/from\s+(\d+\.\d+\.\d+\.\d+)/)?.[1] : null
412
+ };
413
+ }
414
+ return { action: rule.includes('ALLOW') ? 'allow' : 'deny', raw: rule };
415
+ });
347
416
  } catch (err) {
348
417
  status.enabled = false;
349
418
  }
@@ -352,7 +421,22 @@ DNS1=${dns}
352
421
  const fwStatus = await execCommand('firewall-cmd --state');
353
422
  status.enabled = fwStatus === 'running';
354
423
  const rules = await execCommand('firewall-cmd --list-all');
355
- status.rules = [rules];
424
+
425
+ const portsLine = rules.split('\n').find(line => line.trim().startsWith('ports:'));
426
+ if (portsLine) {
427
+ const ports = portsLine.replace('ports:', '').trim();
428
+ if (ports) {
429
+ status.rules = ports.split(/\s+/).map(port => {
430
+ const [portNum, protocol] = port.split('/');
431
+ return {
432
+ action: 'allow',
433
+ port: portNum,
434
+ protocol: protocol || 'tcp',
435
+ ip: null
436
+ };
437
+ });
438
+ }
439
+ }
356
440
  } catch (err) {
357
441
  status.enabled = false;
358
442
  }
@@ -300,7 +300,7 @@
300
300
  async function logout() {
301
301
  if (confirm('确定要退出登录吗?')) {
302
302
  await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
303
- window.location.href = '..';
303
+ window.location.href = '/foxadmin';
304
304
  }
305
305
  }
306
306
 
package/lib/deploy.html CHANGED
@@ -344,6 +344,18 @@
344
344
  window.location.href = '/foxadmin';
345
345
  }
346
346
  }
347
+ async function checkAuth() {
348
+ try {
349
+ const res = await fetch('/foxcontrol_api/admin/check-auth');
350
+ const data = await res.json();
351
+ if (data.status === 'error' || !data.authenticated) {
352
+ window.location.href = '/foxadmin';
353
+ }
354
+ } catch (err) {
355
+ console.error('[Fox Admin] 登录状态检查失败:', err);
356
+ }
357
+ }
358
+ checkAuth();
347
359
  </script>
348
360
  </body>
349
361
  </html>
package/lib/firewall.html CHANGED
@@ -428,14 +428,19 @@
428
428
  }
429
429
 
430
430
  container.innerHTML = firewallStatus.rules.map((rule, index) => {
431
- const isAllow = rule.action === 'allow' || rule.includes('ALLOW');
431
+ const isAllow = rule.action === 'allow';
432
+ const portInfo = rule.port ? `${rule.port}/${rule.protocol || 'tcp'}` : (rule.raw || '未知');
433
+ const ipInfo = rule.ip ? `来自: ${rule.ip}` : '';
434
+
432
435
  return `
433
436
  <div class="rule-item">
434
437
  <div class="rule-info">
435
438
  <span class="rule-action ${isAllow ? 'rule-allow' : 'rule-deny'}">
436
- ${isAllow ? 'ALLOW' : 'DENY'}
439
+ ${isAllow ? '允许' : '拒绝'}
440
+ </span>
441
+ <span class="rule-detail">
442
+ 端口: ${portInfo} ${ipInfo}
437
443
  </span>
438
- <span class="rule-detail">${typeof rule === 'string' ? rule : JSON.stringify(rule)}</span>
439
444
  </div>
440
445
  </div>
441
446
  `;
@@ -508,6 +513,24 @@
508
513
  return;
509
514
  }
510
515
 
516
+ const currentPort = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
517
+ const currentProtocol = window.location.protocol === 'https:' ? 'tcp' : 'tcp';
518
+
519
+ if (action === 'deny' && port == currentPort && protocol === currentProtocol) {
520
+ if (!ip) {
521
+ if (!confirm('警告:您正在拒绝当前访问的端口(' + port + '/' + protocol + '),这将导致您无法继续访问管理界面!\n\n确定要继续吗?')) {
522
+ return;
523
+ }
524
+ } else {
525
+ const currentIP = window.location.hostname;
526
+ if (isValidIP(currentIP) && currentIP === ip) {
527
+ if (!confirm('警告:您正在拒绝当前 IP(' + ip + ')访问端口(' + port + '/' + protocol + '),这将导致您无法继续访问管理界面!\n\n确定要继续吗?')) {
528
+ return;
529
+ }
530
+ }
531
+ }
532
+ }
533
+
511
534
  btn.disabled = true;
512
535
  btn.textContent = '添加中...';
513
536
 
@@ -560,7 +583,7 @@
560
583
  async function logout() {
561
584
  if (confirm('确定要退出登录吗?')) {
562
585
  await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
563
- window.location.href = '..';
586
+ window.location.href = '/foxadmin';
564
587
  }
565
588
  }
566
589
 
package/lib/network.html CHANGED
@@ -407,7 +407,7 @@
407
407
  const data = await res.json();
408
408
 
409
409
  if (data.status === 'success') {
410
- interfacesData = data.data.filter(iface => !iface.internal && !iface.virtual);
410
+ interfacesData = data.data.filter(iface => !iface.internal);
411
411
  renderInterfaces();
412
412
  populateInterfaceSelect();
413
413
 
@@ -461,6 +461,18 @@
461
461
  const select = document.getElementById('interface-select');
462
462
  select.innerHTML = '<option value="">选择接口</option>' +
463
463
  interfacesData.map(iface => `<option value="${iface.iface}">${iface.iface} (${iface.ip4 || '无 IP'})</option>`).join('');
464
+
465
+ select.addEventListener('change', function() {
466
+ const selectedIface = this.value;
467
+ if (!selectedIface) return;
468
+
469
+ const ifaceData = interfacesData.find(iface => iface.iface === selectedIface);
470
+ if (ifaceData) {
471
+ document.getElementById('ip-address').value = ifaceData.ip4 || '';
472
+ document.getElementById('netmask').value = ifaceData.ip4_subnet || '';
473
+ document.getElementById('gateway').value = ifaceData.gateway || '';
474
+ }
475
+ });
464
476
  }
465
477
 
466
478
  function formatSpeed(bytes) {
@@ -491,26 +503,65 @@
491
503
  btn.disabled = true;
492
504
  btn.textContent = '配置中...';
493
505
 
494
- try {
495
- const res = await fetch('/foxcontrol_api/admin/network/config', {
496
- method: 'PUT',
497
- headers: { 'Content-Type': 'application/json' },
498
- body: JSON.stringify({ iface, ip4, netmask, gateway, dns })
499
- });
500
-
501
- const data = await res.json();
506
+ const currentHost = window.location.hostname;
507
+ const currentPort = window.location.port || '1880';
508
+ const currentProtocol = window.location.protocol;
502
509
 
503
- if (data.status === 'success') {
510
+ fetch('/foxcontrol_api/admin/network/config', {
511
+ method: 'PUT',
512
+ headers: { 'Content-Type': 'application/json' },
513
+ body: JSON.stringify({ iface, ip4, netmask, gateway, dns })
514
+ }).then(() => {
515
+ if (currentHost === ip4) {
504
516
  showAlert('网络配置已更新!', 'success');
505
517
  setTimeout(loadInterfaces, 2000);
506
- } else {
507
- throw new Error(data.message || '配置失败');
518
+ btn.disabled = false;
519
+ btn.textContent = '应用配置';
508
520
  }
509
- } catch (err) {
510
- showAlert('配置失败: ' + err.message, 'error');
511
- } finally {
512
- btn.disabled = false;
513
- btn.textContent = '应用配置';
521
+ }).catch(() => {
522
+ if (currentHost === ip4) {
523
+ showAlert('网络配置已更新!', 'success');
524
+ setTimeout(loadInterfaces, 2000);
525
+ btn.disabled = false;
526
+ btn.textContent = '应用配置';
527
+ }
528
+ });
529
+
530
+ if (currentHost !== ip4) {
531
+ showAlert(`网络配置正在应用,5秒后开始检测新地址...`, 'success');
532
+
533
+ setTimeout(async () => {
534
+ showAlert(`正在检测新地址: http://${ip4}:${currentPort}/foxadmin/`, 'success');
535
+
536
+ let checkCount = 0;
537
+ const maxChecks = 30;
538
+ const checkInterval = 2000;
539
+
540
+ const checkNewIP = setInterval(async () => {
541
+ checkCount++;
542
+
543
+ try {
544
+ const testRes = await fetch(`${currentProtocol}//${ip4}:${currentPort}/foxcontrol_api/admin/check-auth`, {
545
+ mode: 'no-cors',
546
+ cache: 'no-cache',
547
+ signal: AbortSignal.timeout(3000)
548
+ });
549
+
550
+ clearInterval(checkInterval);
551
+ showAlert(`新地址已可用!正在跳转到: http://${ip4}:${currentPort}/foxadmin/`, 'success');
552
+
553
+ setTimeout(() => {
554
+ window.location.href = `${currentProtocol}//${ip4}:${currentPort}/foxadmin/`;
555
+ }, 2000);
556
+
557
+ } catch (err) {
558
+ if (checkCount >= maxChecks) {
559
+ clearInterval(checkInterval);
560
+ showAlert(`检测超时,请手动访问新地址: http://${ip4}:${currentPort}/foxadmin/`, 'success');
561
+ }
562
+ }
563
+ }, checkInterval);
564
+ }, 5000);
514
565
  }
515
566
  }
516
567
 
@@ -538,7 +589,7 @@
538
589
  async function logout() {
539
590
  if (confirm('确定要退出登录吗?')) {
540
591
  await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
541
- window.location.href = '..';
592
+ window.location.href = '/foxadmin';
542
593
  }
543
594
  }
544
595
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-fox-admin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Fox Control 系统管理控制台,支持设备管理、网络配置、防火墙管理",
5
5
  "main": "index.js",
6
6
  "scripts": {