node-red-contrib-fox-admin 1.0.0

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.
@@ -0,0 +1,584 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fox Control | 防火墙管理</title>
7
+ <link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MCA2MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJGYWVsaW5rIEljb24iPg0KICA8c3R5bGU+DQogICAgOnJvb3Qgew0KICAgICAgLS1jLTMwMDogIzkzQzVGRDsNCiAgICAgIC0tYy00MDA6ICM2MEE1RkE7DQogICAgICAtLWMtNTAwOiAjM0I4MkY2Ow0KICAgICAgLS1jLTYwMDogIzI1NjNFQjsNCiAgICAgIC0tYy03MDA6ICMxRDRFRDg7DQogICAgICAtLWMtODAwOiAjMUU0MEFGOw0KICAgIH0NCiAgPC9zdHlsZT4NCiAgDQogIDxnIGlkPSJmb3gtbG9nby12MyI+DQogICAgICA8IS0tID09PSBDRU5URVIgU1RSVUNUVVJFID09PSAtLT4NCiAgICAgIDwhLS0gTm9zZSBCcmlkZ2UgKERpYW1vbmQpIC0tPg0KICAgICAgPHBhdGggZD0iTSAzMCAxNSBMIDM2IDI1IEwgMzAgMzggTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICANCiAgICAgIDwhLS0gU25vdXQgVGlwIChTaGFycCBWKSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMjQgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNjAwKSIgLz4gPCEtLSBMZWZ0IFNoYWRvdyAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMzYgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4gPCEtLSBSaWdodCBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFQVJTIChMYXJnZSAmIEFsZXJ0KSA9PT0gLS0+DQogICAgICA8IS0tIExlZnQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyMCAyMCBMIDI2IDE1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4NCiAgICAgIDxwYXRoIGQ9Ik0gMjYgMTUgTCAyMCAyMCBMIDI0IDI1IEwgMzAgMTUgWiIgZmlsbD0idmFyKC0tYy00MDApIiAvPiA8IS0tIElubmVyIGNvbm5lY3Rpb24gLS0+DQogICAgICANCiAgICAgIDwhLS0gUmlnaHQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA1MiAyIEwgNDAgMjAgTCAzNCAxNSBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDM0IDE1IEwgNDAgMjAgTCAzNiAyNSBMIDMwIDE1IFoiIGZpbGw9InZhcigtLWMtMzAwKSIgLz4gPCEtLSBJbm5lciBjb25uZWN0aW9uIC0tPg0KDQogICAgICA8IS0tID09PSBDSEVFS1MgKFRoZSAiRm94IiBTaGFwZSkgPT09IC0tPg0KICAgICAgPCEtLSBMZWZ0IENoZWVrIChXaWRlc3QgUG9pbnQpIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyIDIwIEwgMjQgMjUgTCAyMCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDIgMjAgTCAzMCA1NSBMIDI0IDI1IFoiIGZpbGw9InZhcigtLWMtNzAwKSIgLz4gPCEtLSBTaGFycCBKYXdsaW5lIFNoYWRvdyAtLT4NCiAgICAgIA0KICAgICAgPCEtLSBSaWdodCBDaGVlayAoV2lkZXN0IFBvaW50KSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gNTIgMiBMIDU4IDIwIEwgMzYgMjUgTCA0MCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDU4IDIwIEwgMzAgNTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTUwMCkiIC8+IDwhLS0gU2hhcnAgSmF3bGluZSBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFWUVTIChJbnRlZ3JhdGVkIERlcHRoKSA9PT0gLS0+DQogICAgICA8cGF0aCBkPSJNIDIwIDIwIEwgMjYgMTUgTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTgwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDQwIDIwIEwgMzQgMTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTcwMCkiIC8+DQogIDwvZz4NCjwvc3ZnPg==" type="image/x-icon">
8
+ <style>
9
+ :root {
10
+ --primary: #ed1c24;
11
+ --primary-rgb: 237, 28, 36;
12
+ --success: #52c41a;
13
+ --warning: #faad14;
14
+ --error: #ff4d4f;
15
+ --text-main: #ffffff;
16
+ --text-muted: rgba(255, 255, 255, 0.7);
17
+ --bg-gradient: linear-gradient(135deg, #1e2a38 0%, #2c3e50 100%);
18
+ --card-bg: rgba(255, 255, 255, 0.08);
19
+ --card-border: rgba(255, 255, 255, 0.15);
20
+ }
21
+
22
+ * { box-sizing: border-box; outline: none; margin: 0; padding: 0; }
23
+
24
+ body {
25
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
26
+ background: var(--bg-gradient);
27
+ color: var(--text-main);
28
+ min-height: 100vh;
29
+ }
30
+
31
+ .sidebar {
32
+ position: fixed; left: 0; top: 0;
33
+ width: 250px; height: 100vh;
34
+ background: rgba(0, 0, 0, 0.3);
35
+ backdrop-filter: blur(20px);
36
+ border-right: 1px solid var(--card-border);
37
+ padding: 30px 20px;
38
+ z-index: 100;
39
+ }
40
+
41
+ .logo {
42
+ margin-bottom: 40px; display: flex; align-items: center; justify-content: center;
43
+ }
44
+
45
+ .logo-img {
46
+ width: 40px;
47
+ height: 40px;
48
+ }
49
+
50
+ .nav-item {
51
+ display: flex; align-items: center; gap: 12px;
52
+ padding: 14px 16px;
53
+ margin-bottom: 8px;
54
+ border-radius: 12px;
55
+ cursor: pointer;
56
+ transition: all 0.3s ease;
57
+ color: var(--text-muted);
58
+ text-decoration: none;
59
+ }
60
+
61
+ .nav-item:hover, .nav-item.active {
62
+ background: var(--primary);
63
+ color: white;
64
+ box-shadow: 0 4px 15px rgba(var(--primary-rgb), 0.3);
65
+ }
66
+
67
+ .nav-icon { font-size: 20px; }
68
+
69
+ .main-content {
70
+ margin-left: 250px;
71
+ padding: 30px;
72
+ }
73
+
74
+ .header {
75
+ display: flex; justify-content: space-between; align-items: center;
76
+ margin-bottom: 30px;
77
+ }
78
+
79
+ .header h1 { font-size: 28px; font-weight: 700; }
80
+
81
+ .logout-btn {
82
+ background: rgba(255, 77, 79, 0.2);
83
+ color: var(--error);
84
+ border: 1px solid var(--error);
85
+ padding: 10px 20px;
86
+ border-radius: 8px;
87
+ cursor: pointer;
88
+ transition: all 0.3s;
89
+ }
90
+
91
+ .logout-btn:hover {
92
+ background: var(--error);
93
+ color: white;
94
+ }
95
+
96
+ .card {
97
+ background: var(--card-bg);
98
+ backdrop-filter: blur(20px);
99
+ border: 1px solid var(--card-border);
100
+ border-radius: 16px;
101
+ padding: 24px;
102
+ margin-bottom: 20px;
103
+ }
104
+
105
+ .card h3 { margin-bottom: 20px; font-size: 18px; }
106
+
107
+ .status-badge {
108
+ display: inline-flex; align-items: center; gap: 8px;
109
+ padding: 8px 16px;
110
+ border-radius: 20px;
111
+ font-size: 14px;
112
+ font-weight: 600;
113
+ }
114
+
115
+ .status-enabled {
116
+ background: rgba(82, 196, 38, 0.2);
117
+ color: var(--success);
118
+ border: 1px solid var(--success);
119
+ }
120
+
121
+ .status-disabled {
122
+ background: rgba(255, 77, 79, 0.2);
123
+ color: var(--error);
124
+ border: 1px solid var(--error);
125
+ }
126
+
127
+ .status-dot {
128
+ width: 8px; height: 8px;
129
+ border-radius: 50%;
130
+ background: currentColor;
131
+ animation: pulse 2s infinite;
132
+ }
133
+
134
+ @keyframes pulse {
135
+ 0%, 100% { opacity: 1; }
136
+ 50% { opacity: 0.5; }
137
+ }
138
+
139
+ .control-buttons {
140
+ display: flex; gap: 10px; margin-top: 15px;
141
+ }
142
+
143
+ .btn {
144
+ padding: 10px 20px;
145
+ border: none;
146
+ border-radius: 10px;
147
+ font-size: 14px;
148
+ font-weight: 600;
149
+ cursor: pointer;
150
+ transition: all 0.3s ease;
151
+ }
152
+
153
+ .btn-success {
154
+ background: var(--success);
155
+ color: white;
156
+ box-shadow: 0 4px 15px rgba(82, 196, 38, 0.3);
157
+ }
158
+
159
+ .btn-success:hover {
160
+ transform: translateY(-2px);
161
+ box-shadow: 0 6px 20px rgba(82, 196, 38, 0.4);
162
+ }
163
+
164
+ .btn-danger {
165
+ background: var(--error);
166
+ color: white;
167
+ box-shadow: 0 4px 15px rgba(255, 77, 79, 0.3);
168
+ }
169
+
170
+ .btn-danger:hover {
171
+ transform: translateY(-2px);
172
+ box-shadow: 0 6px 20px rgba(255, 77, 79, 0.4);
173
+ }
174
+
175
+ .btn:disabled {
176
+ opacity: 0.5;
177
+ cursor: not-allowed;
178
+ transform: none;
179
+ }
180
+
181
+ .rule-form {
182
+ display: grid;
183
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
184
+ gap: 15px;
185
+ margin-bottom: 20px;
186
+ }
187
+
188
+ .form-group { margin-bottom: 0; }
189
+
190
+ label {
191
+ display: block; margin-bottom: 8px;
192
+ font-size: 14px; font-weight: 600;
193
+ color: var(--text-muted);
194
+ }
195
+
196
+ input[type="text"], input[type="number"], select {
197
+ width: 100%; padding: 12px 16px;
198
+ background: rgba(255, 255, 255, 0.05);
199
+ border: 2px solid rgba(255, 255, 255, 0.1);
200
+ border-radius: 10px;
201
+ color: white; font-size: 15px;
202
+ transition: all 0.3s ease;
203
+ }
204
+
205
+ input[type="text"]:focus, input[type="number"]:focus, select:focus {
206
+ border-color: var(--primary);
207
+ background: rgba(255, 255, 255, 0.08);
208
+ }
209
+
210
+ select option {
211
+ background: #2c3e50;
212
+ color: white;
213
+ }
214
+
215
+ .btn-primary {
216
+ background: var(--primary);
217
+ color: white;
218
+ box-shadow: 0 4px 15px rgba(var(--primary-rgb), 0.3);
219
+ }
220
+
221
+ .btn-primary:hover {
222
+ transform: translateY(-2px);
223
+ box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.4);
224
+ }
225
+
226
+ .rules-list {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 10px;
230
+ }
231
+
232
+ .rule-item {
233
+ background: rgba(255, 255, 255, 0.03);
234
+ padding: 15px 20px;
235
+ border-radius: 10px;
236
+ border: 1px solid rgba(255, 255, 255, 0.05);
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ }
241
+
242
+ .rule-info {
243
+ display: flex;
244
+ gap: 20px;
245
+ align-items: center;
246
+ }
247
+
248
+ .rule-action {
249
+ padding: 4px 12px;
250
+ border-radius: 12px;
251
+ font-size: 12px;
252
+ font-weight: 700;
253
+ }
254
+
255
+ .rule-allow {
256
+ background: rgba(82, 196, 38, 0.2);
257
+ color: var(--success);
258
+ }
259
+
260
+ .rule-deny {
261
+ background: rgba(255, 77, 79, 0.2);
262
+ color: var(--error);
263
+ }
264
+
265
+ .rule-detail {
266
+ font-size: 14px;
267
+ color: var(--text-muted);
268
+ }
269
+
270
+ .alert {
271
+ padding: 15px 20px;
272
+ border-radius: 10px;
273
+ margin-bottom: 20px;
274
+ display: none;
275
+ }
276
+
277
+ .alert-success {
278
+ background: rgba(82, 196, 38, 0.15);
279
+ border: 1px solid var(--success);
280
+ color: #b7eb8f;
281
+ }
282
+
283
+ .alert-error {
284
+ background: rgba(255, 77, 79, 0.15);
285
+ border: 1px solid var(--error);
286
+ color: #ffccc7;
287
+ }
288
+
289
+ .loading { text-align: center; padding: 40px; color: var(--text-muted); }
290
+ </style>
291
+ </head>
292
+ <body>
293
+ <div class="sidebar">
294
+ <div class="logo">
295
+ <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MCA2MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJGYWVsaW5rIEljb24iPg0KICA8c3R5bGU+DQogICAgOnJvb3Qgew0KICAgICAgLS1jLTMwMDogIzkzQzVGRDsNCiAgICAgIC0tYy00MDA6ICM2MEE1RkE7DQogICAgICAtLWMtNTAwOiAjM0I4MkY2Ow0KICAgICAgLS1jLTYwMDogIzI1NjNFQjsNCiAgICAgIC0tYy03MDA6ICMxRDRFRDg7DQogICAgICAtLWMtODAwOiAjMUU0MEFGOw0KICAgIH0NCiAgPC9zdHlsZT4NCiAgDQogIDxnIGlkPSJmb3gtbG9nby12MyI+DQogICAgICA8IS0tID09PSBDRU5URVIgU1RSVUNUVVJFID09PSAtLT4NCiAgICAgIDwhLS0gTm9zZSBCcmlkZ2UgKERpYW1vbmQpIC0tPg0KICAgICAgPHBhdGggZD0iTSAzMCAxNSBMIDM2IDI1IEwgMzAgMzggTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICANCiAgICAgIDwhLS0gU25vdXQgVGlwIChTaGFycCBWKSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMjQgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNjAwKSIgLz4gPCEtLSBMZWZ0IFNoYWRvdyAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMzYgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4gPCEtLSBSaWdodCBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFQVJTIChMYXJnZSAmIEFsZXJ0KSA9PT0gLS0+DQogICAgICA8IS0tIExlZnQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyMCAyMCBMIDI2IDE1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4NCiAgICAgIDxwYXRoIGQ9Ik0gMjYgMTUgTCAyMCAyMCBMIDI0IDI1IEwgMzAgMTUgWiIgZmlsbD0idmFyKC0tYy00MDApIiAvPiA8IS0tIElubmVyIGNvbm5lY3Rpb24gLS0+DQogICAgICANCiAgICAgIDwhLS0gUmlnaHQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA1MiAyIEwgNDAgMjAgTCAzNCAxNSBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDM0IDE1IEwgNDAgMjAgTCAzNiAyNSBMIDMwIDE1IFoiIGZpbGw9InZhcigtLWMtMzAwKSIgLz4gPCEtLSBJbm5lciBjb25uZWN0aW9uIC0tPg0KDQogICAgICA8IS0tID09PSBDSEVFS1MgKFRoZSAiRm94IiBTaGFwZSkgPT09IC0tPg0KICAgICAgPCEtLSBMZWZ0IENoZWVrIChXaWRlc3QgUG9pbnQpIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyIDIwIEwgMjQgMjUgTCAyMCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDIgMjAgTCAzMCA1NSBMIDI0IDI1IFoiIGZpbGw9InZhcigtLWMtNzAwKSIgLz4gPCEtLSBTaGFycCBKYXdsaW5lIFNoYWRvdyAtLT4NCiAgICAgIA0KICAgICAgPCEtLSBSaWdodCBDaGVlayAoV2lkZXN0IFBvaW50KSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gNTIgMiBMIDU4IDIwIEwgMzYgMjUgTCA0MCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDU4IDIwIEwgMzAgNTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTUwMCkiIC8+IDwhLS0gU2hhcnAgSmF3bGluZSBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFWUVTIChJbnRlZ3JhdGVkIERlcHRoKSA9PT0gLS0+DQogICAgICA8cGF0aCBkPSJNIDIwIDIwIEwgMjYgMTUgTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTgwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDQwIDIwIEwgMzQgMTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTcwMCkiIC8+DQogIDwvZz4NCjwvc3ZnPg==" class="logo-img" alt="Fox Logo">
296
+ </div>
297
+ <a href="foxcontroladmin_dashboard" class="nav-item">
298
+ <span class="nav-icon">📊</span>
299
+ <span>控制台</span>
300
+ </a>
301
+ <a href="foxcontroladmin_network" class="nav-item">
302
+ <span class="nav-icon">🌐</span>
303
+ <span>网络管理</span>
304
+ </a>
305
+ <a href="foxcontroladmin_firewall" class="nav-item active">
306
+ <span class="nav-icon">🔒</span>
307
+ <span>防火墙</span>
308
+ </a>
309
+ <a href="foxcontroladmin_deploy" class="nav-item">
310
+ <span class="nav-icon">🚀</span>
311
+ <span>站点部署</span>
312
+ </a>
313
+ <a href="../foxcontrol_edit" class="nav-item" target="_blank">
314
+ <span class="nav-icon">⚡</span>
315
+ <span>流程编辑器</span>
316
+ </a>
317
+ </div>
318
+
319
+ <div class="main-content">
320
+ <div class="header">
321
+ <h1>防火墙管理</h1>
322
+ <button class="logout-btn" onclick="logout()">退出登录</button>
323
+ </div>
324
+
325
+ <div id="alert-success" class="alert alert-success"></div>
326
+ <div id="alert-error" class="alert alert-error"></div>
327
+
328
+ <div class="card">
329
+ <h3>防火墙状态</h3>
330
+ <div id="loading" class="loading">加载中...</div>
331
+ <div id="firewall-status" style="display: none;">
332
+ <div id="status-badge" class="status-badge"></div>
333
+ <div class="control-buttons">
334
+ <button class="btn btn-success" id="enable-btn" onclick="enableFirewall()">启用防火墙</button>
335
+ <button class="btn btn-danger" id="disable-btn" onclick="disableFirewall()">禁用防火墙</button>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="card">
341
+ <h3>添加规则</h3>
342
+ <div class="rule-form">
343
+ <div class="form-group">
344
+ <label for="rule-action">动作</label>
345
+ <select id="rule-action">
346
+ <option value="allow">允许</option>
347
+ <option value="deny">拒绝</option>
348
+ </select>
349
+ </div>
350
+ <div class="form-group">
351
+ <label for="rule-port">端口</label>
352
+ <input type="number" id="rule-port" placeholder="例如: 80" min="1" max="65535">
353
+ </div>
354
+ <div class="form-group">
355
+ <label for="rule-protocol">协议</label>
356
+ <select id="rule-protocol">
357
+ <option value="tcp">TCP</option>
358
+ <option value="udp">UDP</option>
359
+ </select>
360
+ </div>
361
+ <div class="form-group">
362
+ <label for="rule-ip">IP 地址(可选)</label>
363
+ <input type="text" id="rule-ip" placeholder="例如: 192.168.1.100">
364
+ </div>
365
+ </div>
366
+ <button class="btn btn-primary" id="add-rule-btn" onclick="addRule()">添加规则</button>
367
+ </div>
368
+
369
+ <div class="card">
370
+ <h3>当前规则</h3>
371
+ <div id="rules-list" class="rules-list"></div>
372
+ </div>
373
+ </div>
374
+
375
+ <script>
376
+ let firewallStatus = { enabled: false, rules: [] };
377
+
378
+ async function loadFirewallStatus() {
379
+ try {
380
+ const res = await fetch('/foxcontrol_api/admin/firewall/status');
381
+ const data = await res.json();
382
+
383
+ // 无论成功还是失败,都要隐藏加载中提示
384
+ document.getElementById('loading').style.display = 'none';
385
+
386
+ if (data.status === 'success') {
387
+ firewallStatus = data.data;
388
+ renderFirewallStatus();
389
+ renderRules();
390
+ document.getElementById('firewall-status').style.display = 'block';
391
+ } else {
392
+ // 处理后端返回的业务错误(如:不支持的系统)
393
+ document.getElementById('loading').textContent = '加载失败: ' + (data.message || '未知错误');
394
+ document.getElementById('loading').style.display = 'block';
395
+ showAlert(data.message, 'error');
396
+ }
397
+ } catch (err) {
398
+ // 处理网络层面的错误
399
+ document.getElementById('loading').textContent = '网络错误: ' + err.message;
400
+ document.getElementById('loading').style.display = 'block';
401
+ }
402
+ }
403
+
404
+ function renderFirewallStatus() {
405
+ const badge = document.getElementById('status-badge');
406
+ const enableBtn = document.getElementById('enable-btn');
407
+ const disableBtn = document.getElementById('disable-btn');
408
+
409
+ if (firewallStatus.enabled) {
410
+ badge.className = 'status-badge status-enabled';
411
+ badge.innerHTML = '<span class="status-dot"></span> 防火墙已启用';
412
+ enableBtn.disabled = true;
413
+ disableBtn.disabled = false;
414
+ } else {
415
+ badge.className = 'status-badge status-disabled';
416
+ badge.innerHTML = '<span class="status-dot"></span> 防火墙已禁用';
417
+ enableBtn.disabled = false;
418
+ disableBtn.disabled = true;
419
+ }
420
+ }
421
+
422
+ function renderRules() {
423
+ const container = document.getElementById('rules-list');
424
+
425
+ if (!firewallStatus.rules || firewallStatus.rules.length === 0) {
426
+ container.innerHTML = '<div style="text-align: center; color: var(--text-muted); padding: 20px;">暂无规则</div>';
427
+ return;
428
+ }
429
+
430
+ container.innerHTML = firewallStatus.rules.map((rule, index) => {
431
+ const isAllow = rule.action === 'allow' || rule.includes('ALLOW');
432
+ return `
433
+ <div class="rule-item">
434
+ <div class="rule-info">
435
+ <span class="rule-action ${isAllow ? 'rule-allow' : 'rule-deny'}">
436
+ ${isAllow ? 'ALLOW' : 'DENY'}
437
+ </span>
438
+ <span class="rule-detail">${typeof rule === 'string' ? rule : JSON.stringify(rule)}</span>
439
+ </div>
440
+ </div>
441
+ `;
442
+ }).join('');
443
+ }
444
+
445
+ async function enableFirewall() {
446
+ const btn = document.getElementById('enable-btn');
447
+ btn.disabled = true;
448
+ btn.textContent = '启用中...';
449
+
450
+ try {
451
+ const res = await fetch('/foxcontrol_api/admin/firewall/enable', { method: 'PUT' });
452
+ const data = await res.json();
453
+
454
+ if (data.status === 'success') {
455
+ showAlert('防火墙已启用', 'success');
456
+ setTimeout(loadFirewallStatus, 1000);
457
+ } else {
458
+ throw new Error(data.message || '启用失败');
459
+ }
460
+ } catch (err) {
461
+ showAlert('启用失败: ' + err.message, 'error');
462
+ btn.disabled = false;
463
+ btn.textContent = '启用防火墙';
464
+ }
465
+ }
466
+
467
+ async function disableFirewall() {
468
+ const btn = document.getElementById('disable-btn');
469
+ btn.disabled = true;
470
+ btn.textContent = '禁用中...';
471
+
472
+ try {
473
+ const res = await fetch('/foxcontrol_api/admin/firewall/disable', { method: 'PUT' });
474
+ const data = await res.json();
475
+
476
+ if (data.status === 'success') {
477
+ showAlert('防火墙已禁用', 'success');
478
+ setTimeout(loadFirewallStatus, 1000);
479
+ } else {
480
+ throw new Error(data.message || '禁用失败');
481
+ }
482
+ } catch (err) {
483
+ showAlert('禁用失败: ' + err.message, 'error');
484
+ btn.disabled = false;
485
+ btn.textContent = '禁用防火墙';
486
+ }
487
+ }
488
+
489
+ async function addRule() {
490
+ const action = document.getElementById('rule-action').value;
491
+ const port = document.getElementById('rule-port').value.trim();
492
+ const protocol = document.getElementById('rule-protocol').value;
493
+ const ip = document.getElementById('rule-ip').value.trim();
494
+ const btn = document.getElementById('add-rule-btn');
495
+
496
+ if (!port) {
497
+ showAlert('请输入端口号', 'error');
498
+ return;
499
+ }
500
+
501
+ if (port < 1 || port > 65535) {
502
+ showAlert('端口号必须在 1-65535 之间', 'error');
503
+ return;
504
+ }
505
+
506
+ if (ip && !isValidIP(ip)) {
507
+ showAlert('IP 地址格式不正确', 'error');
508
+ return;
509
+ }
510
+
511
+ btn.disabled = true;
512
+ btn.textContent = '添加中...';
513
+
514
+ try {
515
+ const res = await fetch('/foxcontrol_api/admin/firewall/rule', {
516
+ method: 'POST',
517
+ headers: { 'Content-Type': 'application/json' },
518
+ body: JSON.stringify({ action, port: parseInt(port), protocol, ip: ip || null })
519
+ });
520
+
521
+ const data = await res.json();
522
+
523
+ if (data.status === 'success') {
524
+ showAlert('规则已添加', 'success');
525
+ document.getElementById('rule-port').value = '';
526
+ document.getElementById('rule-ip').value = '';
527
+ setTimeout(loadFirewallStatus, 1000);
528
+ } else {
529
+ throw new Error(data.message || '添加失败');
530
+ }
531
+ } catch (err) {
532
+ showAlert('添加失败: ' + err.message, 'error');
533
+ } finally {
534
+ btn.disabled = false;
535
+ btn.textContent = '添加规则';
536
+ }
537
+ }
538
+
539
+ function isValidIP(ip) {
540
+ const pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
541
+ if (!pattern.test(ip)) return false;
542
+ const parts = ip.split('.');
543
+ return parts.every(part => {
544
+ const num = parseInt(part);
545
+ return num >= 0 && num <= 255;
546
+ });
547
+ }
548
+
549
+ function showAlert(message, type) {
550
+ const alertId = type === 'success' ? 'alert-success' : 'alert-error';
551
+ const alertEl = document.getElementById(alertId);
552
+ alertEl.textContent = message;
553
+ alertEl.style.display = 'block';
554
+
555
+ setTimeout(() => {
556
+ alertEl.style.display = 'none';
557
+ }, 5000);
558
+ }
559
+
560
+ async function logout() {
561
+ if (confirm('确定要退出登录吗?')) {
562
+ await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
563
+ window.location.href = '..';
564
+ }
565
+ }
566
+
567
+ async function checkAuth() {
568
+ try {
569
+ const res = await fetch('/foxcontrol_api/admin/check-auth');
570
+ const data = await res.json();
571
+ if (data.status === 'error' || !data.authenticated) {
572
+ window.location.href = '/foxadmin';
573
+ }
574
+ } catch (err) {
575
+ console.error('[Fox Admin] 登录状态检查失败:', err);
576
+ }
577
+ }
578
+
579
+ loadFirewallStatus();
580
+ checkAuth();
581
+ setInterval(loadFirewallStatus, 30000);
582
+ </script>
583
+ </body>
584
+ </html>