3xui-api-client 2.1.1 → 3.1.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.
- package/CHANGELOG.md +50 -0
- package/README.md +300 -286
- package/index.d.ts +390 -2
- package/index.js +703 -69
- package/package.json +4 -8
- package/src/generators/CredentialGenerator.js +21 -18
- package/src/session/SessionManager.js +24 -1
- package/src/utils/ByteConversion.js +132 -0
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
CredentialSecurity,
|
|
9
9
|
ErrorSecurity
|
|
10
10
|
} = require('./src/security/SecurityEnhancer');
|
|
11
|
+
const { convertBandwidthFields } = require('./src/utils/ByteConversion');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 3X-UI API Client Library
|
|
@@ -32,16 +33,10 @@ class ThreeXUI {
|
|
|
32
33
|
* @param {number} options.timeout - Request timeout in milliseconds (default: 30000)
|
|
33
34
|
* @throws {Error} If baseURL, username, or password is missing
|
|
34
35
|
*/
|
|
35
|
-
constructor(baseURL,
|
|
36
|
+
constructor(baseURL, usernameOrOptions, password, options = {}) {
|
|
36
37
|
if (!baseURL) {
|
|
37
38
|
throw new Error('baseURL is required');
|
|
38
39
|
}
|
|
39
|
-
if (!username) {
|
|
40
|
-
throw new Error('username is required');
|
|
41
|
-
}
|
|
42
|
-
if (!password) {
|
|
43
|
-
throw new Error('password is required');
|
|
44
|
-
}
|
|
45
40
|
|
|
46
41
|
// Apply security validations
|
|
47
42
|
this.baseURL = InputValidator.validateURL(baseURL);
|
|
@@ -53,13 +48,46 @@ class ThreeXUI {
|
|
|
53
48
|
this.baseURL = this.baseURL.replace(/\/panel\/?$/, '');
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
// Object containing configs as 2nd parameter
|
|
52
|
+
if (typeof usernameOrOptions === 'object' && usernameOrOptions !== null) {
|
|
53
|
+
options = usernameOrOptions;
|
|
54
|
+
this.username = options.username;
|
|
55
|
+
this.password = options.password;
|
|
56
|
+
} else {
|
|
57
|
+
this.username = usernameOrOptions;
|
|
58
|
+
this.password = password;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.token = options.token || options.apiToken || null;
|
|
62
|
+
|
|
63
|
+
if (this.token) {
|
|
64
|
+
this.username = this.username || 'token-auth'; // prevent missing args error
|
|
65
|
+
this.password = this.password || 'token-auth';
|
|
66
|
+
} else {
|
|
67
|
+
if (!this.username) {
|
|
68
|
+
throw new Error('username is required');
|
|
69
|
+
}
|
|
70
|
+
if (!this.password) {
|
|
71
|
+
throw new Error('password is required');
|
|
72
|
+
}
|
|
73
|
+
this.username = InputValidator.validateUsername(this.username);
|
|
74
|
+
this.password = InputValidator.validatePassword(this.password);
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
this.cookie = null;
|
|
78
|
+
this.csrfToken = null;
|
|
59
79
|
this.options = options;
|
|
60
80
|
this.loginMutex = false; // Add mutex to prevent concurrent logins
|
|
61
81
|
this.loginRetryCount = 0; // Add retry counter
|
|
62
82
|
this.maxLoginRetries = 3; // Maximum login attempts
|
|
83
|
+
// Delay (ms) between forced re-login attempts on 401.
|
|
84
|
+
// Helps avoid tripping fail2ban or 3x-ui login-rate limits,
|
|
85
|
+
// especially in serverless environments where many cold-start
|
|
86
|
+
// instances may race to re-authenticate simultaneously.
|
|
87
|
+
this.loginRetryBackoff = options.loginRetryBackoff !== undefined
|
|
88
|
+
? options.loginRetryBackoff
|
|
89
|
+
: 500;
|
|
90
|
+
this.panelType = options.panelType || 'auto'; // 'modern', 'legacy', or 'auto' for detection
|
|
63
91
|
|
|
64
92
|
// Initialize security monitoring
|
|
65
93
|
this.securityMonitor = new SecurityMonitor({
|
|
@@ -81,15 +109,21 @@ class ThreeXUI {
|
|
|
81
109
|
}
|
|
82
110
|
|
|
83
111
|
// Create axios instance with security best practices
|
|
112
|
+
const secureHeaders = SecureHeaders.getSecureHeaders({
|
|
113
|
+
userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
|
|
114
|
+
enableCSP: options.enableCSP || false
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (this.token) {
|
|
118
|
+
secureHeaders['Authorization'] = `Bearer ${this.token}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
84
121
|
this.api = axios.create({
|
|
85
122
|
baseURL: this.baseURL,
|
|
86
123
|
timeout: options.timeout || 30000, // 30 second timeout
|
|
87
124
|
maxRedirects: 5,
|
|
88
125
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
89
|
-
headers:
|
|
90
|
-
userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
|
|
91
|
-
enableCSP: options.enableCSP || false
|
|
92
|
-
})
|
|
126
|
+
headers: secureHeaders
|
|
93
127
|
});
|
|
94
128
|
|
|
95
129
|
// Add request interceptor for security headers
|
|
@@ -120,6 +154,14 @@ class ThreeXUI {
|
|
|
120
154
|
* @returns {Object} Login response
|
|
121
155
|
*/
|
|
122
156
|
async login(forceRefresh = false) {
|
|
157
|
+
// If API token is configured, skip cookie auth
|
|
158
|
+
if (this.token) {
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
message: 'Authenticated successfully using API Token'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
// Check rate limiting first
|
|
124
166
|
const identifier = CredentialSecurity.hashForLogging(this.username);
|
|
125
167
|
if (!this.securityMonitor.checkRateLimit(identifier, 'login')) {
|
|
@@ -136,6 +178,13 @@ class ThreeXUI {
|
|
|
136
178
|
if (existingSession && existingSession.cookie) {
|
|
137
179
|
this.cookie = existingSession.cookie;
|
|
138
180
|
this.api.defaults.headers.Cookie = this.cookie;
|
|
181
|
+
if (existingSession.csrfToken) {
|
|
182
|
+
this.csrfToken = existingSession.csrfToken;
|
|
183
|
+
}
|
|
184
|
+
// Restore panel type from session if auto-detection was used
|
|
185
|
+
if (existingSession.panelType && this.panelType === 'auto') {
|
|
186
|
+
this.panelType = existingSession.panelType;
|
|
187
|
+
}
|
|
139
188
|
// Reset retry count on successful session restore
|
|
140
189
|
this.loginRetryCount = 0;
|
|
141
190
|
return {
|
|
@@ -151,11 +200,66 @@ class ThreeXUI {
|
|
|
151
200
|
params.append('username', this.username);
|
|
152
201
|
params.append('password', this.password);
|
|
153
202
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
const loginHeaders = {
|
|
204
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Newer (React-based) 3x-ui panels require a CSRF token + session
|
|
208
|
+
// cookie obtained from /csrf-token before /login will accept
|
|
209
|
+
// credentials. Older (Vue-based) panels don't expose this
|
|
210
|
+
// endpoint (404), in which case we fall back to the classic
|
|
211
|
+
// direct login below.
|
|
212
|
+
const csrfInfo = await this._getCsrfToken();
|
|
213
|
+
if (csrfInfo) {
|
|
214
|
+
loginHeaders['X-CSRF-Token'] = csrfInfo.token;
|
|
215
|
+
loginHeaders['Cookie'] = csrfInfo.cookie;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Try modern endpoint first (/panel/api/login), then fall back to legacy endpoint (/login)
|
|
219
|
+
// This supports both newer (React-based) and older (Vue-based) 3x-ui panels
|
|
220
|
+
let response;
|
|
221
|
+
let lastError;
|
|
222
|
+
let detectedPanelType = this.panelType;
|
|
223
|
+
|
|
224
|
+
// Determine which endpoint to try based on panelType setting
|
|
225
|
+
const tryModernFirst = this.panelType !== 'legacy';
|
|
226
|
+
const tryLegacyFallback = this.panelType !== 'modern';
|
|
227
|
+
|
|
228
|
+
if (tryModernFirst) {
|
|
229
|
+
// Try modern endpoint first (/panel/api/login)
|
|
230
|
+
try {
|
|
231
|
+
response = await this.api.post('/panel/api/login', params, {
|
|
232
|
+
headers: loginHeaders
|
|
233
|
+
});
|
|
234
|
+
if (this.panelType === 'auto') {
|
|
235
|
+
detectedPanelType = 'modern';
|
|
236
|
+
}
|
|
237
|
+
} catch (modernError) {
|
|
238
|
+
lastError = modernError;
|
|
239
|
+
// If modern endpoint fails and fallback is enabled, try legacy endpoint
|
|
240
|
+
if (tryLegacyFallback) {
|
|
241
|
+
try {
|
|
242
|
+
response = await this.api.post('/login', params, {
|
|
243
|
+
headers: loginHeaders
|
|
244
|
+
});
|
|
245
|
+
if (this.panelType === 'auto') {
|
|
246
|
+
detectedPanelType = 'legacy';
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Both endpoints failed, throw the last error
|
|
250
|
+
throw lastError;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
throw modernError;
|
|
254
|
+
}
|
|
157
255
|
}
|
|
158
|
-
}
|
|
256
|
+
} else {
|
|
257
|
+
// Try legacy endpoint first (/login) if explicitly set
|
|
258
|
+
response = await this.api.post('/login', params, {
|
|
259
|
+
headers: loginHeaders
|
|
260
|
+
});
|
|
261
|
+
detectedPanelType = 'legacy';
|
|
262
|
+
}
|
|
159
263
|
|
|
160
264
|
if (response.data.success) {
|
|
161
265
|
const cookies = response.headers['set-cookie'];
|
|
@@ -163,11 +267,24 @@ class ThreeXUI {
|
|
|
163
267
|
this.cookie = cookies[0].split(';')[0];
|
|
164
268
|
this.api.defaults.headers.Cookie = this.cookie;
|
|
165
269
|
|
|
270
|
+
// On newer panels, every non-safe request (POST/PUT/DELETE)
|
|
271
|
+
// also requires this CSRF token, not just /login.
|
|
272
|
+
if (csrfInfo) {
|
|
273
|
+
this.csrfToken = csrfInfo.token;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update panel type after successful detection
|
|
277
|
+
if (this.panelType === 'auto') {
|
|
278
|
+
this.panelType = detectedPanelType;
|
|
279
|
+
}
|
|
280
|
+
|
|
166
281
|
// Store session if session manager is available
|
|
167
282
|
if (this.sessionManager) {
|
|
168
283
|
try {
|
|
169
284
|
await this.sessionManager.storeSession(this.baseURL, this.username, {
|
|
170
285
|
cookie: this.cookie,
|
|
286
|
+
csrfToken: this.csrfToken,
|
|
287
|
+
panelType: detectedPanelType, // Store detected panel type for future use
|
|
171
288
|
loginTime: new Date().toISOString()
|
|
172
289
|
});
|
|
173
290
|
} catch (sessionError) {
|
|
@@ -211,6 +328,43 @@ class ThreeXUI {
|
|
|
211
328
|
}
|
|
212
329
|
}
|
|
213
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Detect newer (React-based) 3x-ui panels and obtain the CSRF token +
|
|
333
|
+
* session cookie required by their /login endpoint.
|
|
334
|
+
*
|
|
335
|
+
* Older (Vue-based) panels don't expose /csrf-token and respond with a
|
|
336
|
+
* 404, in which case this returns null so login() falls back to the
|
|
337
|
+
* classic direct login flow.
|
|
338
|
+
*
|
|
339
|
+
* @returns {Promise<{token: string, cookie: string} | null>}
|
|
340
|
+
*/
|
|
341
|
+
async _getCsrfToken() {
|
|
342
|
+
try {
|
|
343
|
+
const response = await this.api.get('/csrf-token', {
|
|
344
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
345
|
+
validateStatus: (status) => status === 200 || status === 404
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (response.status !== 200 || !response.data || !response.data.success || !response.data.obj) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const cookies = response.headers['set-cookie'];
|
|
353
|
+
if (!cookies || cookies.length === 0) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
token: response.data.obj,
|
|
359
|
+
cookie: cookies[0].split(';')[0]
|
|
360
|
+
};
|
|
361
|
+
} catch {
|
|
362
|
+
// /csrf-token unreachable or errored - treat as an old panel
|
|
363
|
+
// and let login() proceed with the classic flow.
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
214
368
|
/**
|
|
215
369
|
* Logout and clear session
|
|
216
370
|
*/
|
|
@@ -222,6 +376,7 @@ class ThreeXUI {
|
|
|
222
376
|
}
|
|
223
377
|
|
|
224
378
|
this.cookie = null;
|
|
379
|
+
this.csrfToken = null;
|
|
225
380
|
delete this.api.defaults.headers.Cookie;
|
|
226
381
|
|
|
227
382
|
if (this.sessionManager) {
|
|
@@ -237,12 +392,14 @@ class ThreeXUI {
|
|
|
237
392
|
return this._request('post', '/getTwoFactorEnable');
|
|
238
393
|
}
|
|
239
394
|
|
|
240
|
-
async _request(method, path, data = {}) {
|
|
241
|
-
// Check session validity first with mutex protection
|
|
242
|
-
if (!this.
|
|
243
|
-
await this.
|
|
244
|
-
|
|
245
|
-
|
|
395
|
+
async _request(method, path, data = {}, extraHeaders = {}) {
|
|
396
|
+
// Check session validity first with mutex protection if token is not provided
|
|
397
|
+
if (!this.token) {
|
|
398
|
+
if (!this.loginMutex && this.sessionManager && !await this.sessionManager.hasValidSession(this.baseURL, this.username)) {
|
|
399
|
+
await this._ensureAuthenticated();
|
|
400
|
+
} else if (!this.loginMutex && !this.cookie) {
|
|
401
|
+
await this._ensureAuthenticated();
|
|
402
|
+
}
|
|
246
403
|
}
|
|
247
404
|
|
|
248
405
|
try {
|
|
@@ -250,21 +407,30 @@ class ThreeXUI {
|
|
|
250
407
|
method,
|
|
251
408
|
url: path,
|
|
252
409
|
data,
|
|
253
|
-
|
|
410
|
+
headers: { ...this._buildRequestHeaders(method), ...extraHeaders }
|
|
254
411
|
});
|
|
255
412
|
// Reset retry counter on successful request
|
|
256
413
|
this.loginRetryCount = 0;
|
|
257
414
|
return response.data;
|
|
258
415
|
} catch (error) {
|
|
259
416
|
if (error.response && error.response.status === 401) {
|
|
260
|
-
|
|
417
|
+
if (this.token) {
|
|
418
|
+
throw new Error('API Token is invalid or expired. Please check your credentials.');
|
|
419
|
+
}
|
|
420
|
+
// Cookie might have expired, try to login again with retry limit.
|
|
421
|
+
// A configurable backoff delay is applied before each re-login to
|
|
422
|
+
// avoid burning through the retry budget or tripping login-rate limits
|
|
423
|
+
// (e.g. fail2ban) when many instances race to re-authenticate.
|
|
261
424
|
if (this.loginRetryCount < this.maxLoginRetries) {
|
|
425
|
+
if (this.loginRetryBackoff > 0) {
|
|
426
|
+
await new Promise(resolve => setTimeout(resolve, this.loginRetryBackoff));
|
|
427
|
+
}
|
|
262
428
|
await this._ensureAuthenticated(true); // Force refresh
|
|
263
429
|
const response = await this.api.request({
|
|
264
430
|
method,
|
|
265
431
|
url: path,
|
|
266
432
|
data,
|
|
267
|
-
|
|
433
|
+
headers: { ...this._buildRequestHeaders(method), ...extraHeaders }
|
|
268
434
|
});
|
|
269
435
|
return response.data;
|
|
270
436
|
} else {
|
|
@@ -275,6 +441,30 @@ class ThreeXUI {
|
|
|
275
441
|
}
|
|
276
442
|
}
|
|
277
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Build per-request headers, including the JSON content type for POST
|
|
446
|
+
* bodies and (on cookie-authenticated sessions against newer
|
|
447
|
+
* React-based panels) the X-CSRF-Token header required for non-safe
|
|
448
|
+
* HTTP methods.
|
|
449
|
+
*
|
|
450
|
+
* @param {string} method - HTTP method for the request
|
|
451
|
+
* @returns {Object} Headers to merge into the request
|
|
452
|
+
*/
|
|
453
|
+
_buildRequestHeaders(method) {
|
|
454
|
+
const headers = {};
|
|
455
|
+
|
|
456
|
+
if (method.toLowerCase() === 'post') {
|
|
457
|
+
headers['Content-Type'] = 'application/json';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const isSafeMethod = ['get', 'head', 'options'].includes(method.toLowerCase());
|
|
461
|
+
if (!isSafeMethod && !this.token && this.csrfToken) {
|
|
462
|
+
headers['X-CSRF-Token'] = this.csrfToken;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return headers;
|
|
466
|
+
}
|
|
467
|
+
|
|
278
468
|
/**
|
|
279
469
|
* Ensure authentication with mutex protection
|
|
280
470
|
* @param {boolean} forceRefresh - Force a new login
|
|
@@ -406,6 +596,7 @@ class ThreeXUI {
|
|
|
406
596
|
*/
|
|
407
597
|
async addClientWithCredentials(inboundId, protocol, options = {}) {
|
|
408
598
|
const credentials = this.generateCredentials(protocol, options);
|
|
599
|
+
const convertedOptions = convertBandwidthFields(options);
|
|
409
600
|
|
|
410
601
|
const clientConfig = {
|
|
411
602
|
id: inboundId,
|
|
@@ -413,10 +604,10 @@ class ThreeXUI {
|
|
|
413
604
|
clients: [{
|
|
414
605
|
...credentials,
|
|
415
606
|
enable: true,
|
|
416
|
-
expiryTime:
|
|
417
|
-
limitIp:
|
|
418
|
-
totalGB:
|
|
419
|
-
subId:
|
|
607
|
+
expiryTime: convertedOptions.expiryTime || 0,
|
|
608
|
+
limitIp: convertedOptions.limitIp || 0,
|
|
609
|
+
totalGB: convertedOptions.totalGB || 0,
|
|
610
|
+
subId: convertedOptions.subId || this.generateUUID()
|
|
420
611
|
}]
|
|
421
612
|
})
|
|
422
613
|
};
|
|
@@ -455,17 +646,19 @@ class ThreeXUI {
|
|
|
455
646
|
throw new Error(`Client with ID ${clientId} not found in inbound ${inboundId}`);
|
|
456
647
|
}
|
|
457
648
|
|
|
649
|
+
// Convert bandwidth fields (GB to bytes)
|
|
650
|
+
const convertedOptions = convertBandwidthFields(options);
|
|
651
|
+
|
|
458
652
|
// Convert user-friendly options to API format
|
|
459
653
|
const processedOptions = {
|
|
460
|
-
email:
|
|
461
|
-
limitIp:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
subId: options.subId || existingClients[clientIndex].subId
|
|
654
|
+
email: convertedOptions.email || existingClients[clientIndex].email,
|
|
655
|
+
limitIp: convertedOptions.limitIp !== undefined ? convertedOptions.limitIp : existingClients[clientIndex].limitIp,
|
|
656
|
+
totalGB: convertedOptions.totalGB !== undefined ? convertedOptions.totalGB : existingClients[clientIndex].totalGB,
|
|
657
|
+
expiryTime: convertedOptions.expiryDays ? Date.now() + (convertedOptions.expiryDays * 24 * 60 * 60 * 1000) : existingClients[clientIndex].expiryTime,
|
|
658
|
+
enable: convertedOptions.enable !== undefined ? convertedOptions.enable : existingClients[clientIndex].enable,
|
|
659
|
+
flow: convertedOptions.flow || existingClients[clientIndex].flow,
|
|
660
|
+
encryption: convertedOptions.encryption || existingClients[clientIndex].encryption || 'none',
|
|
661
|
+
subId: convertedOptions.subId || existingClients[clientIndex].subId
|
|
469
662
|
};
|
|
470
663
|
|
|
471
664
|
// Update the specific client while preserving others
|
|
@@ -490,8 +683,8 @@ class ThreeXUI {
|
|
|
490
683
|
...result,
|
|
491
684
|
updatedOptions: processedOptions,
|
|
492
685
|
conversions: {
|
|
493
|
-
totalGB:
|
|
494
|
-
expiryDays:
|
|
686
|
+
totalGB: convertedOptions.totalGB !== undefined ? `${options.totalGB} GB → ${convertedOptions.totalGB} bytes` : 'unchanged',
|
|
687
|
+
expiryDays: convertedOptions.expiryDays ? `${convertedOptions.expiryDays} days → ${new Date(processedOptions.expiryTime).toISOString()}` : 'unchanged'
|
|
495
688
|
}
|
|
496
689
|
};
|
|
497
690
|
} catch (error) {
|
|
@@ -533,12 +726,334 @@ class ThreeXUI {
|
|
|
533
726
|
* @returns {boolean} Session validity
|
|
534
727
|
*/
|
|
535
728
|
async isSessionValid() {
|
|
729
|
+
if (this.token) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
536
732
|
if (this.sessionManager) {
|
|
537
733
|
return await this.sessionManager.hasValidSession(this.baseURL, this.username);
|
|
538
734
|
}
|
|
539
735
|
return !!this.cookie;
|
|
540
736
|
}
|
|
541
737
|
|
|
738
|
+
// ===========================================
|
|
739
|
+
// MODERN API METHODS (3X-UI >= 2.x)
|
|
740
|
+
// ===========================================
|
|
741
|
+
|
|
742
|
+
// --- Clients ---
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get list of all clients
|
|
746
|
+
* @returns {Promise<Object>} Formatted list of all clients
|
|
747
|
+
*/
|
|
748
|
+
getClients() {
|
|
749
|
+
return this._request('get', '/panel/api/clients/list');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Get paginated list of clients
|
|
754
|
+
* @param {Object} params - Pagination parameters
|
|
755
|
+
* @param {number} params.page - Page number (default: 1)
|
|
756
|
+
* @param {number} params.size - Items per page (default: 10)
|
|
757
|
+
* @param {string} params.sort - Sort field (e.g., 'email', 'expireTime')
|
|
758
|
+
* @param {string} params.order - Sort order ('asc' or 'desc')
|
|
759
|
+
* @param {string} params.email - Filter by email
|
|
760
|
+
* @returns {Promise<Object>} Paginated clients
|
|
761
|
+
*/
|
|
762
|
+
getPagedClients(params = {}) {
|
|
763
|
+
const queryParams = new URLSearchParams();
|
|
764
|
+
if (params.page !== undefined) {
|
|
765
|
+
queryParams.append('page', params.page);
|
|
766
|
+
}
|
|
767
|
+
if (params.size !== undefined) {
|
|
768
|
+
queryParams.append('size', params.size);
|
|
769
|
+
}
|
|
770
|
+
if (params.sort !== undefined) {
|
|
771
|
+
queryParams.append('sort', params.sort);
|
|
772
|
+
}
|
|
773
|
+
if (params.order !== undefined) {
|
|
774
|
+
queryParams.append('order', params.order);
|
|
775
|
+
}
|
|
776
|
+
if (params.email !== undefined) {
|
|
777
|
+
queryParams.append('email', params.email);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const queryString = queryParams.toString();
|
|
781
|
+
const url = queryString ? `/panel/api/clients/list/paged?${queryString}` : '/panel/api/clients/list/paged';
|
|
782
|
+
|
|
783
|
+
return this._request('get', url);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Get client by email
|
|
788
|
+
* @param {string} email - Exact client email address
|
|
789
|
+
* @returns {Promise<Object>} Client metadata
|
|
790
|
+
*/
|
|
791
|
+
getClient(email) {
|
|
792
|
+
return this._request('get', `/panel/api/clients/get/${encodeURIComponent(email)}`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Get client traffic by email
|
|
797
|
+
* @param {string} email - Exact client email
|
|
798
|
+
* @returns {Promise<Object>} Client traffic details
|
|
799
|
+
*/
|
|
800
|
+
getClientTraffic(email) {
|
|
801
|
+
return this._request('get', `/panel/api/clients/traffic/${encodeURIComponent(email)}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Get subscription links for a client by subscription ID
|
|
806
|
+
* @param {string} subId - Subscription ID (UUID)
|
|
807
|
+
* @returns {Promise<Object>} Subscription details and links
|
|
808
|
+
*/
|
|
809
|
+
getSubLinks(subId) {
|
|
810
|
+
return this._request('get', `/panel/api/clients/subLinks/${encodeURIComponent(subId)}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Get generic client links by email
|
|
815
|
+
* @param {string} email - Exact client email
|
|
816
|
+
* @returns {Promise<Object>} Link strings
|
|
817
|
+
*/
|
|
818
|
+
getClientLinks(email) {
|
|
819
|
+
return this._request('get', `/panel/api/clients/links/${encodeURIComponent(email)}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Add a new client via Modern API
|
|
824
|
+
* @param {Object} data - Client payload
|
|
825
|
+
* @returns {Promise<Object>} Addition response
|
|
826
|
+
*/
|
|
827
|
+
addModernClient(data) {
|
|
828
|
+
const convertedData = {
|
|
829
|
+
...data,
|
|
830
|
+
client: data.client ? convertBandwidthFields(data.client) : undefined
|
|
831
|
+
};
|
|
832
|
+
return this._request('post', '/panel/api/clients/add', convertedData);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Update client by email via Modern API
|
|
837
|
+
* @param {string} email - Exact client email
|
|
838
|
+
* @param {Object} data - Update payload
|
|
839
|
+
* @returns {Promise<Object>} Update response
|
|
840
|
+
*/
|
|
841
|
+
updateModernClient(email, data) {
|
|
842
|
+
const convertedData = convertBandwidthFields(data);
|
|
843
|
+
return this._request('post', `/panel/api/clients/update/${encodeURIComponent(email)}`, convertedData);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Delete client by email via Modern API
|
|
848
|
+
* @param {string} email - Exact client email
|
|
849
|
+
* @returns {Promise<Object>} Delete response
|
|
850
|
+
*/
|
|
851
|
+
deleteModernClient(email) {
|
|
852
|
+
return this._request('post', `/panel/api/clients/del/${encodeURIComponent(email)}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
attachClientToInbounds(email, data) {
|
|
856
|
+
return this._request('post', `/panel/api/clients/${encodeURIComponent(email)}/attach`, data);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
detachClientFromInbounds(email, data) {
|
|
860
|
+
return this._request('post', `/panel/api/clients/${encodeURIComponent(email)}/detach`, data);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
resetAllModernClientTraffics() {
|
|
864
|
+
return this._request('post', '/panel/api/clients/resetAllTraffics');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
deleteDepletedModernClients() {
|
|
868
|
+
return this._request('post', '/panel/api/clients/delDepleted');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
bulkAdjustModernClients(data) {
|
|
872
|
+
return this._request('post', '/panel/api/clients/bulkAdjust', data);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
bulkDeleteModernClients(data) {
|
|
876
|
+
return this._request('post', '/panel/api/clients/bulkDel', data);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
bulkCreateModernClients(data) {
|
|
880
|
+
return this._request('post', '/panel/api/clients/bulkCreate', data);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
bulkAttachModernClients(data) {
|
|
884
|
+
return this._request('post', '/panel/api/clients/bulkAttach', data);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
bulkDetachModernClients(data) {
|
|
888
|
+
return this._request('post', '/panel/api/clients/bulkDetach', data);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
bulkResetTrafficModernClients(data) {
|
|
892
|
+
return this._request('post', '/panel/api/clients/bulkResetTraffic', data);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
resetModernClientTrafficByEmail(email) {
|
|
896
|
+
return this._request('post', `/panel/api/clients/resetTraffic/${encodeURIComponent(email)}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
updateModernClientTrafficByEmail(email, data) {
|
|
900
|
+
return this._request('post', `/panel/api/clients/updateTraffic/${encodeURIComponent(email)}`, data);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
getModernClientIps(email) {
|
|
904
|
+
return this._request('post', `/panel/api/clients/ips/${encodeURIComponent(email)}`);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
clearModernClientIps(email) {
|
|
908
|
+
return this._request('post', `/panel/api/clients/clearIps/${encodeURIComponent(email)}`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
getOnlines() {
|
|
912
|
+
return this._request('post', '/panel/api/clients/onlines');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
getModernLastOnline() {
|
|
916
|
+
return this._request('post', '/panel/api/clients/lastOnline');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// --- Client Groups ---
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Get list of all client groups
|
|
923
|
+
* @returns {Promise<Object>} List of client groups
|
|
924
|
+
*/
|
|
925
|
+
getGroups() {
|
|
926
|
+
return this._request('get', '/panel/api/clients/groups');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get list of emails belonging to a specific group
|
|
931
|
+
* @param {string} groupName - The name of the group
|
|
932
|
+
* @returns {Promise<Object>} List of emails in the group
|
|
933
|
+
*/
|
|
934
|
+
getGroupEmails(groupName) {
|
|
935
|
+
return this._request('get', `/panel/api/clients/groups/${encodeURIComponent(groupName)}/emails`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
createGroup(data) {
|
|
939
|
+
return this._request('post', '/panel/api/clients/groups/create', data);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
renameGroup(data) {
|
|
943
|
+
return this._request('post', '/panel/api/clients/groups/rename', data);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
deleteGroup(data) {
|
|
947
|
+
return this._request('post', '/panel/api/clients/groups/delete', data);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
bulkAddGroups(data) {
|
|
951
|
+
return this._request('post', '/panel/api/clients/groups/bulkAdd', data);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
bulkRemoveGroups(data) {
|
|
955
|
+
return this._request('post', '/panel/api/clients/groups/bulkRemove', data);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// --- Nodes ---
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Get list of all nodes
|
|
962
|
+
* @returns {Promise<Object>} List of nodes
|
|
963
|
+
*/
|
|
964
|
+
getNodes() {
|
|
965
|
+
return this._request('get', '/panel/api/nodes/list');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Get specific node by ID
|
|
970
|
+
* @param {number|string} id - Node ID
|
|
971
|
+
* @returns {Promise<Object>} Node details
|
|
972
|
+
*/
|
|
973
|
+
getNode(id) {
|
|
974
|
+
return this._request('get', `/panel/api/nodes/get/${encodeURIComponent(id)}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Get history metrics for a node
|
|
979
|
+
* @param {number|string} id - Node ID
|
|
980
|
+
* @param {string} metric - Metric name (e.g., 'cpu', 'memory')
|
|
981
|
+
* @param {string} bucket - Time bucket size
|
|
982
|
+
* @returns {Promise<Object>} Node history data
|
|
983
|
+
*/
|
|
984
|
+
getNodeHistory(id, metric, bucket) {
|
|
985
|
+
return this._request('get', `/panel/api/nodes/history/${encodeURIComponent(id)}/${encodeURIComponent(metric)}/${encodeURIComponent(bucket)}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
addNode(data) {
|
|
989
|
+
return this._request('post', '/panel/api/nodes/add', data);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
updateNode(id, data) {
|
|
993
|
+
return this._request('post', `/panel/api/nodes/update/${encodeURIComponent(id)}`, data);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
deleteNode(id) {
|
|
997
|
+
return this._request('post', `/panel/api/nodes/del/${encodeURIComponent(id)}`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Enable or disable a node.
|
|
1002
|
+
* @param {number|string} id - Node ID
|
|
1003
|
+
* @param {boolean} [enable] - Desired enabled state. If omitted, the panel toggles the current state.
|
|
1004
|
+
* @returns {Promise<Object>} Result of the operation
|
|
1005
|
+
*/
|
|
1006
|
+
setNodeEnable(id, enable) {
|
|
1007
|
+
const data = typeof enable === 'boolean' ? { enable } : {};
|
|
1008
|
+
return this._request('post', `/panel/api/nodes/setEnable/${encodeURIComponent(id)}`, data);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
testNode(data) {
|
|
1012
|
+
return this._request('post', '/panel/api/nodes/test', data);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
probeNode(id) {
|
|
1016
|
+
return this._request('post', `/panel/api/nodes/probe/${encodeURIComponent(id)}`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// --- Custom Geo ---
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Get list of custom geo sites/ips
|
|
1023
|
+
* @returns {Promise<Object>} List of custom geos
|
|
1024
|
+
*/
|
|
1025
|
+
getCustomGeos() {
|
|
1026
|
+
return this._request('get', '/panel/api/custom-geo/list');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Get aliases for custom geos
|
|
1031
|
+
* @returns {Promise<Object>} Custom geo aliases
|
|
1032
|
+
*/
|
|
1033
|
+
getGeoAliases() {
|
|
1034
|
+
return this._request('get', '/panel/api/custom-geo/aliases');
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
addCustomGeo(data) {
|
|
1038
|
+
return this._request('post', '/panel/api/custom-geo/add', data);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
updateCustomGeo(id, data) {
|
|
1042
|
+
return this._request('post', `/panel/api/custom-geo/update/${encodeURIComponent(id)}`, data);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
deleteCustomGeo(id) {
|
|
1046
|
+
return this._request('post', `/panel/api/custom-geo/delete/${encodeURIComponent(id)}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
downloadCustomGeo(id) {
|
|
1050
|
+
return this._request('post', `/panel/api/custom-geo/download/${encodeURIComponent(id)}`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
updateAllCustomGeo() {
|
|
1054
|
+
return this._request('post', '/panel/api/custom-geo/update-all');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
542
1057
|
// ===========================================
|
|
543
1058
|
// ORIGINAL API METHODS (UNCHANGED)
|
|
544
1059
|
// ===========================================
|
|
@@ -569,11 +1084,21 @@ class ThreeXUI {
|
|
|
569
1084
|
}
|
|
570
1085
|
|
|
571
1086
|
/**
|
|
572
|
-
* Import inbounds
|
|
573
|
-
*
|
|
1087
|
+
* Import one or more inbounds. The panel endpoint only accepts a single
|
|
1088
|
+
* inbound per request (as a form-encoded `data` field containing the
|
|
1089
|
+
* inbound JSON), so each inbound is sent as a separate request.
|
|
1090
|
+
* @param {Object|Object[]} inbounds - Inbound configuration, or array of configurations
|
|
1091
|
+
* @returns {Promise<Object[]>} One response object per imported inbound
|
|
574
1092
|
*/
|
|
575
1093
|
importInbounds(inbounds) {
|
|
576
|
-
|
|
1094
|
+
const list = Array.isArray(inbounds) ? inbounds : [inbounds];
|
|
1095
|
+
return Promise.all(list.map((inboundConfig) => {
|
|
1096
|
+
const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
|
|
1097
|
+
const body = new URLSearchParams({ data: JSON.stringify(validatedConfig) }).toString();
|
|
1098
|
+
return this._request('post', '/panel/api/inbounds/import', body, {
|
|
1099
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
1100
|
+
});
|
|
1101
|
+
}));
|
|
577
1102
|
}
|
|
578
1103
|
|
|
579
1104
|
/**
|
|
@@ -680,10 +1205,10 @@ class ThreeXUI {
|
|
|
680
1205
|
|
|
681
1206
|
/**
|
|
682
1207
|
* Get CPU usage history
|
|
683
|
-
* @param {
|
|
1208
|
+
* @param {number} bucket - Bucket size in seconds. Must be one of: 2, 30, 60, 120, 180, 300
|
|
684
1209
|
*/
|
|
685
|
-
getCPUHistory(bucket =
|
|
686
|
-
return this._request('get', `/panel/api/server/cpuHistory/${bucket}`);
|
|
1210
|
+
getCPUHistory(bucket = 60) {
|
|
1211
|
+
return this._request('get', `/panel/api/server/cpuHistory/${encodeURIComponent(bucket)}`);
|
|
687
1212
|
}
|
|
688
1213
|
|
|
689
1214
|
/**
|
|
@@ -702,9 +1227,24 @@ class ThreeXUI {
|
|
|
702
1227
|
|
|
703
1228
|
/**
|
|
704
1229
|
* Download database
|
|
1230
|
+
* @returns {Promise<string>} Raw SQLite database file content as a string (starts with "SQLite format 3 ..."), not a Buffer
|
|
705
1231
|
*/
|
|
706
|
-
getDb() {
|
|
707
|
-
|
|
1232
|
+
async getDb() {
|
|
1233
|
+
const response = await this._request('get', '/panel/api/server/getDb');
|
|
1234
|
+
// Add response validation
|
|
1235
|
+
if (!response || typeof response !== 'object') {
|
|
1236
|
+
throw new Error('getDb: Invalid response format');
|
|
1237
|
+
}
|
|
1238
|
+
if (response.success === false) {
|
|
1239
|
+
throw new Error(`getDb failed: ${response.msg || 'Unknown error'}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (!response.obj) {
|
|
1242
|
+
throw new Error('getDb: Response missing database content');
|
|
1243
|
+
}
|
|
1244
|
+
if (typeof response.obj !== 'string') {
|
|
1245
|
+
throw new Error(`getDb: Expected string content, got ${typeof response.obj}`);
|
|
1246
|
+
}
|
|
1247
|
+
return response;
|
|
708
1248
|
}
|
|
709
1249
|
|
|
710
1250
|
/**
|
|
@@ -723,9 +1263,20 @@ class ThreeXUI {
|
|
|
723
1263
|
|
|
724
1264
|
/**
|
|
725
1265
|
* Install specific Xray version
|
|
726
|
-
* @param {string} version - Version to install
|
|
1266
|
+
* @param {string} version - Version to install (e.g., "1.8.0")
|
|
727
1267
|
*/
|
|
728
1268
|
installXray(version) {
|
|
1269
|
+
// Validate version parameter
|
|
1270
|
+
if (!version || typeof version !== 'string') {
|
|
1271
|
+
throw new Error('installXray: version must be a non-empty string');
|
|
1272
|
+
}
|
|
1273
|
+
if (version.toLowerCase() === 'latest') {
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
'installXray: "latest" is not a valid version. ' +
|
|
1276
|
+
'Use client.getXrayVersion() to get available versions, ' +
|
|
1277
|
+
'then pass a specific version like "1.8.0"'
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
729
1280
|
return this._request('post', `/panel/api/server/installXray/${version}`);
|
|
730
1281
|
}
|
|
731
1282
|
|
|
@@ -793,8 +1344,25 @@ class ThreeXUI {
|
|
|
793
1344
|
return this._request('get', '/panel/api/server/getNewVlessEnc');
|
|
794
1345
|
}
|
|
795
1346
|
|
|
796
|
-
|
|
797
|
-
|
|
1347
|
+
/**
|
|
1348
|
+
* Generate a new ECH (Encrypted Client Hello) certificate.
|
|
1349
|
+
* @param {string} [sni] - Optional SNI to embed in the generated certificate.
|
|
1350
|
+
* Sent as `application/x-www-form-urlencoded` since the panel reads it via `c.PostForm`.
|
|
1351
|
+
* @returns {Promise<Object>} Generated ECH certificate data
|
|
1352
|
+
*/
|
|
1353
|
+
getNewEchCert(sni) {
|
|
1354
|
+
const body = sni ? new URLSearchParams({ sni }).toString() : '';
|
|
1355
|
+
return this._request('post', '/panel/api/server/getNewEchCert', body, {
|
|
1356
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Get the panel's configured web certificate/key file paths.
|
|
1362
|
+
* @returns {Promise<Object>} Web certificate file info
|
|
1363
|
+
*/
|
|
1364
|
+
getWebCertFiles() {
|
|
1365
|
+
return this._request('get', '/panel/api/server/getWebCertFiles');
|
|
798
1366
|
}
|
|
799
1367
|
|
|
800
1368
|
// ===========================================
|
|
@@ -805,52 +1373,106 @@ class ThreeXUI {
|
|
|
805
1373
|
* Get all panel settings
|
|
806
1374
|
*/
|
|
807
1375
|
getAllSettings() {
|
|
808
|
-
return this._request('post', '/panel/setting/all');
|
|
1376
|
+
return this._request('post', '/panel/api/setting/all');
|
|
809
1377
|
}
|
|
810
1378
|
|
|
811
1379
|
/**
|
|
812
1380
|
* Update panel settings
|
|
813
|
-
*
|
|
1381
|
+
* NOTE: The API requires ALL settings to be sent together, not just the changed ones.
|
|
1382
|
+
* This method automatically fetches current settings, applies your changes, and sends all.
|
|
1383
|
+
* @param {Object} updates - Settings to update (partial object, will be merged with current)
|
|
1384
|
+
* @returns {Promise} Response from /panel/api/setting/update
|
|
814
1385
|
*/
|
|
815
|
-
updateSetting(
|
|
816
|
-
|
|
1386
|
+
async updateSetting(updates) {
|
|
1387
|
+
if (!updates || typeof updates !== 'object') {
|
|
1388
|
+
throw new Error('updates must be an object');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Fetch current settings
|
|
1392
|
+
const currentResponse = await this.getAllSettings();
|
|
1393
|
+
if (!currentResponse.success || !currentResponse.obj) {
|
|
1394
|
+
throw new Error('Failed to fetch current settings for merge');
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Merge updates with current settings (updates override current)
|
|
1398
|
+
const mergedSettings = {
|
|
1399
|
+
...currentResponse.obj,
|
|
1400
|
+
...updates
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// Send all settings together
|
|
1404
|
+
return this._request('post', '/panel/api/setting/update', mergedSettings);
|
|
817
1405
|
}
|
|
818
1406
|
|
|
819
1407
|
/**
|
|
820
1408
|
* Update admin username and password
|
|
1409
|
+
* ⚠️ CRITICAL: This endpoint changes your login credentials.
|
|
1410
|
+
* After a successful update, the internal client credentials are refreshed
|
|
1411
|
+
* and the session is re-authenticated with new credentials.
|
|
821
1412
|
* @param {string} oldUsername - Current username
|
|
822
1413
|
* @param {string} oldPassword - Current password
|
|
823
1414
|
* @param {string} newUsername - New username
|
|
824
1415
|
* @param {string} newPassword - New password
|
|
825
1416
|
*/
|
|
826
|
-
updateUser(oldUsername, oldPassword, newUsername, newPassword) {
|
|
827
|
-
|
|
1417
|
+
async updateUser(oldUsername, oldPassword, newUsername, newPassword) {
|
|
1418
|
+
const response = await this._request('post', '/panel/api/setting/updateUser', {
|
|
828
1419
|
oldUsername,
|
|
829
1420
|
oldPassword,
|
|
830
1421
|
newUsername,
|
|
831
1422
|
newPassword
|
|
832
1423
|
});
|
|
1424
|
+
|
|
1425
|
+
if (response && response.success) {
|
|
1426
|
+
// Update internal credentials to match new credentials
|
|
1427
|
+
const oldUsername_ = this.username;
|
|
1428
|
+
this.username = newUsername;
|
|
1429
|
+
this.password = newPassword;
|
|
1430
|
+
|
|
1431
|
+
// Clear old session from session manager
|
|
1432
|
+
if (this.sessionManager) {
|
|
1433
|
+
try {
|
|
1434
|
+
await this.sessionManager.deleteSession(this.baseURL, oldUsername_);
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
console.warn('[3xui-api-client] Could not delete old session:', error.message);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Re-authenticate with new credentials to ensure session is valid
|
|
1441
|
+
try {
|
|
1442
|
+
await this.login(true); // Force refresh with new credentials
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
// If login fails after credential change, restore old credentials and re-throw
|
|
1445
|
+
this.username = oldUsername;
|
|
1446
|
+
this.password = oldPassword;
|
|
1447
|
+
if (this.sessionManager) {
|
|
1448
|
+
await this.sessionManager.deleteSession(this.baseURL, newUsername);
|
|
1449
|
+
}
|
|
1450
|
+
throw new Error(`Credential update succeeded but re-authentication failed: ${error.message}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return response;
|
|
833
1455
|
}
|
|
834
1456
|
|
|
835
1457
|
/**
|
|
836
1458
|
* Restart the panel
|
|
837
1459
|
*/
|
|
838
1460
|
restartPanel() {
|
|
839
|
-
return this._request('post', '/panel/setting/restartPanel');
|
|
1461
|
+
return this._request('post', '/panel/api/setting/restartPanel');
|
|
840
1462
|
}
|
|
841
1463
|
|
|
842
1464
|
/**
|
|
843
1465
|
* Get default settings
|
|
844
1466
|
*/
|
|
845
1467
|
getDefaultSettings() {
|
|
846
|
-
return this._request('post', '/panel/setting/defaultSettings');
|
|
1468
|
+
return this._request('post', '/panel/api/setting/defaultSettings');
|
|
847
1469
|
}
|
|
848
1470
|
|
|
849
1471
|
/**
|
|
850
1472
|
* Get default Xray JSON config
|
|
851
1473
|
*/
|
|
852
1474
|
getDefaultJsonConfig() {
|
|
853
|
-
return this._request('get', '/panel/setting/getDefaultJsonConfig');
|
|
1475
|
+
return this._request('get', '/panel/api/setting/getDefaultJsonConfig');
|
|
854
1476
|
}
|
|
855
1477
|
|
|
856
1478
|
// ===========================================
|
|
@@ -861,45 +1483,57 @@ class ThreeXUI {
|
|
|
861
1483
|
* Get Xray configuration
|
|
862
1484
|
*/
|
|
863
1485
|
getXrayConfig() {
|
|
864
|
-
return this._request('post', '/panel/xray/');
|
|
1486
|
+
return this._request('post', '/panel/api/xray/');
|
|
865
1487
|
}
|
|
866
1488
|
|
|
867
1489
|
/**
|
|
868
1490
|
* Update Xray configuration
|
|
869
|
-
* @param {string} config - Xray configuration content
|
|
1491
|
+
* @param {string|object} config - Xray configuration content (JSON string or object)
|
|
870
1492
|
*/
|
|
871
1493
|
updateXrayConfig(config) {
|
|
872
|
-
|
|
1494
|
+
// Parse JSON string to object before sending
|
|
1495
|
+
if (typeof config === 'string') {
|
|
1496
|
+
try {
|
|
1497
|
+
config = JSON.parse(config);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
throw new Error(`updateXrayConfig: Invalid JSON: ${error.message}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
// Validate it's an object
|
|
1503
|
+
if (!config || typeof config !== 'object') {
|
|
1504
|
+
throw new Error('updateXrayConfig: config must be a valid JSON object or string');
|
|
1505
|
+
}
|
|
1506
|
+
return this._request('post', '/panel/api/xray/update', { content: JSON.stringify(config) });
|
|
873
1507
|
}
|
|
874
1508
|
|
|
875
1509
|
/**
|
|
876
1510
|
* Manage WARP
|
|
877
|
-
* @param {string} action - Action to perform (data, del, config, reg, license)
|
|
1511
|
+
* @param {string} action - Action to perform (data, del, config, reg, changeIp, license, interval)
|
|
878
1512
|
* @param {Object} [data] - Additional data for the action
|
|
879
1513
|
*/
|
|
880
1514
|
manageWarp(action, data = {}) {
|
|
881
|
-
return this._request('post', `/panel/xray/warp/${action}`, data);
|
|
1515
|
+
return this._request('post', `/panel/api/xray/warp/${action}`, data);
|
|
882
1516
|
}
|
|
883
1517
|
|
|
884
1518
|
/**
|
|
885
1519
|
* Get outbound traffic statistics
|
|
886
1520
|
*/
|
|
887
1521
|
getOutboundsTraffic() {
|
|
888
|
-
return this._request('get', '/panel/xray/getOutboundsTraffic');
|
|
1522
|
+
return this._request('get', '/panel/api/xray/getOutboundsTraffic');
|
|
889
1523
|
}
|
|
890
1524
|
|
|
891
1525
|
/**
|
|
892
1526
|
* Reset outbound traffic statistics
|
|
893
1527
|
*/
|
|
894
1528
|
resetOutboundsTraffic() {
|
|
895
|
-
return this._request('post', '/panel/xray/resetOutboundsTraffic');
|
|
1529
|
+
return this._request('post', '/panel/api/xray/resetOutboundsTraffic');
|
|
896
1530
|
}
|
|
897
1531
|
|
|
898
1532
|
/**
|
|
899
1533
|
* Get Xray execution result
|
|
900
1534
|
*/
|
|
901
1535
|
getXrayResult() {
|
|
902
|
-
return this._request('get', '/panel/xray/getXrayResult');
|
|
1536
|
+
return this._request('get', '/panel/api/xray/getXrayResult');
|
|
903
1537
|
}
|
|
904
1538
|
|
|
905
1539
|
// Security Methods
|