3xui-api-client
Version:
A Node.js client library for 3x-ui panel API with built-in credential generation, session management, and web integration support
493 lines (432 loc) • 14.1 kB
JavaScript
/**
* Session Management System for 3xui-api-client
* Provides both built-in caching and user-managed database integration
*/
const crypto = require('crypto');
/**
* Abstract base class for session storage
*/
class SessionStore {
async get(_key) {
throw new Error('get method must be implemented');
}
async set(_key, _value, _ttl) {
throw new Error('set method must be implemented');
}
async delete(_key) {
throw new Error('delete method must be implemented');
}
async clear() {
throw new Error('clear method must be implemented');
}
async exists(key) {
const value = await this.get(key);
return value !== null;
}
}
/**
* Built-in memory session store (default)
* Recommended for development and single-instance deployments
*/
class MemorySessionStore extends SessionStore {
constructor() {
super();
this.cache = new Map();
this.timers = new Map();
}
async get(key) {
const item = this.cache.get(key);
if (!item) {
return null;
}
if (item.expires && item.expires <= Date.now()) {
this.delete(key);
return null;
}
return item.value;
}
async set(key, value, ttl = 3600) {
// Clear existing timer if it exists
const existingTimer = this.timers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
const expires = ttl > 0 ? Date.now() + (ttl * 1000) : null;
this.cache.set(key, { value, expires });
// Set expiration timer
if (ttl > 0) {
const timer = setTimeout(() => {
this.delete(key);
}, ttl * 1000);
this.timers.set(key, timer);
}
}
async delete(key) {
this.cache.delete(key);
const timer = this.timers.get(key);
if (timer) {
clearTimeout(timer);
this.timers.delete(key);
}
}
async clear() {
// Clear all timers
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.cache.clear();
this.timers.clear();
}
// Get cache statistics
getStats() {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys())
};
}
}
/**
* Redis session store adapter
* Recommended for production and multi-instance deployments
*/
class RedisSessionStore extends SessionStore {
constructor(redisClient, options = {}) {
super();
this.redis = redisClient;
this.keyPrefix = options.keyPrefix || '3xui:session:';
this.defaultTTL = options.defaultTTL || 3600;
}
_getKey(key) {
return `${this.keyPrefix}${key}`;
}
async get(key) {
try {
const value = await this.redis.get(this._getKey(key));
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Redis get error:', error);
return null;
}
}
async set(key, value, ttl = this.defaultTTL) {
try {
const serialized = JSON.stringify(value);
if (ttl > 0) {
await this.redis.setex(this._getKey(key), ttl, serialized);
} else {
await this.redis.set(this._getKey(key), serialized);
}
} catch (error) {
console.error('Redis set error:', error);
throw error;
}
}
async delete(key) {
try {
await this.redis.del(this._getKey(key));
} catch (error) {
console.error('Redis delete error:', error);
}
}
async clear() {
try {
const keys = await this.redis.keys(`${this.keyPrefix}*`);
if (keys.length > 0) {
await this.redis.del(keys);
}
} catch (error) {
console.error('Redis clear error:', error);
}
}
async exists(key) {
try {
const result = await this.redis.exists(this._getKey(key));
return result === 1;
} catch (error) {
console.error('Redis exists error:', error);
return false;
}
}
}
/**
* Database session store adapter
* Works with any SQL database through a provided database client
*/
class DatabaseSessionStore extends SessionStore {
constructor(database, options = {}) {
super();
this.db = database;
this.tableName = options.tableName || 'sessions';
this.keyColumn = options.keyColumn || 'session_key';
this.valueColumn = options.valueColumn || 'session_data';
this.expiresColumn = options.expiresColumn || 'expires_at';
this.defaultTTL = options.defaultTTL || 3600;
}
async get(key) {
try {
const query = `
SELECT ${this.valueColumn}
FROM ${this.tableName}
WHERE ${this.keyColumn} = ?
AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
`;
const result = await this.db.query(query, [key, new Date()]);
if (result.length === 0) {
return null;
}
return JSON.parse(result[0][this.valueColumn]);
} catch (error) {
console.error('Database get error:', error);
return null;
}
}
async set(key, value, ttl = this.defaultTTL) {
try {
const serialized = JSON.stringify(value);
const expiresAt = ttl > 0 ? new Date(Date.now() + ttl * 1000) : null;
// Use UPSERT operation
const query = `
INSERT INTO ${this.tableName} (${this.keyColumn}, ${this.valueColumn}, ${this.expiresColumn})
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
${this.valueColumn} = VALUES(${this.valueColumn}),
${this.expiresColumn} = VALUES(${this.expiresColumn})
`;
await this.db.query(query, [key, serialized, expiresAt]);
} catch (error) {
console.error('Database set error:', error);
throw error;
}
}
async delete(key) {
try {
const query = `DELETE FROM ${this.tableName} WHERE ${this.keyColumn} = ?`;
await this.db.query(query, [key]);
} catch (error) {
console.error('Database delete error:', error);
}
}
async clear() {
try {
const query = `DELETE FROM ${this.tableName}`;
await this.db.query(query);
} catch (error) {
console.error('Database clear error:', error);
}
}
async exists(key) {
try {
const query = `
SELECT 1 FROM ${this.tableName}
WHERE ${this.keyColumn} = ?
AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
LIMIT 1
`;
const result = await this.db.query(query, [key, new Date()]);
return result.length > 0;
} catch (error) {
console.error('Database exists error:', error);
return false;
}
}
// Cleanup expired sessions
async cleanupExpired() {
try {
const query = `DELETE FROM ${this.tableName} WHERE ${this.expiresColumn} <= ?`;
const result = await this.db.query(query, [new Date()]);
return result.affectedRows || 0;
} catch (error) {
console.error('Database cleanup error:', error);
return 0;
}
}
// Get database schema for creating sessions table
static getCreateTableSQL(tableName = 'sessions') {
return `
CREATE TABLE IF NOT EXISTS ${tableName} (
session_key VARCHAR(255) PRIMARY KEY,
session_data TEXT NOT NULL,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
}
}
/**
* User-managed session handler
* Allows users to provide their own session management functions
*/
class CustomSessionHandler {
constructor(handlers) {
this.getSession = handlers.getSession;
this.setSession = handlers.setSession;
this.deleteSession = handlers.deleteSession;
this.validateSession = handlers.validateSession;
}
async get(key) {
try {
return await this.getSession(key);
} catch (error) {
console.error('Custom session get error:', error);
return null;
}
}
async set(key, value, ttl) {
try {
await this.setSession(key, value, ttl);
} catch (error) {
console.error('Custom session set error:', error);
throw error;
}
}
async delete(key) {
try {
await this.deleteSession(key);
} catch (error) {
console.error('Custom session delete error:', error);
}
}
async validate(key) {
try {
if (this.validateSession) {
return await this.validateSession(key);
}
return await this.get(key) !== null;
} catch (error) {
console.error('Custom session validate error:', error);
return false;
}
}
}
/**
* Main session manager class
*/
class SessionManager {
constructor(options = {}) {
this.sessionTTL = options.sessionTTL || 3600; // 1 hour default
this.autoRefresh = options.autoRefresh !== false;
this.refreshThreshold = options.refreshThreshold || 0.8; // Refresh when 80% expired
// Initialize session store
if (options.store) {
this.store = options.store;
} else if (options.redis) {
this.store = new RedisSessionStore(options.redis, options.redisOptions);
} else if (options.database) {
this.store = new DatabaseSessionStore(options.database, options.databaseOptions);
} else if (options.customHandler) {
this.store = new CustomSessionHandler(options.customHandler);
} else {
this.store = new MemorySessionStore();
}
}
/**
* Generate session key for a server
*/
generateSessionKey(baseURL, username) {
const hash = crypto
.createHash('sha256')
.update(`${baseURL}:${username}`)
.digest('hex');
return `session_${hash}`;
}
/**
* Store session data
*/
async storeSession(baseURL, username, sessionData) {
const key = this.generateSessionKey(baseURL, username);
const data = {
...sessionData,
createdAt: Date.now(),
baseURL,
username
};
await this.store.set(key, data, this.sessionTTL);
return key;
}
/**
* Retrieve session data
*/
async getSession(baseURL, username) {
const key = this.generateSessionKey(baseURL, username);
return await this.store.get(key);
}
/**
* Check if session exists and is valid
*/
async hasValidSession(baseURL, username) {
const session = await this.getSession(baseURL, username);
if (!session) {
return false;
}
// Check if session needs refresh
if (this.autoRefresh && this.shouldRefreshSession(session)) {
return false; // Trigger re-authentication
}
return true;
}
/**
* Check if session should be refreshed
*/
shouldRefreshSession(session) {
if (!session.createdAt) {
return true;
}
const age = Date.now() - session.createdAt;
const maxAge = this.sessionTTL * 1000;
const threshold = maxAge * this.refreshThreshold;
return age >= threshold;
}
/**
* Delete session
*/
async deleteSession(baseURL, username) {
const key = this.generateSessionKey(baseURL, username);
await this.store.delete(key);
}
/**
* Clear all sessions
*/
async clearAllSessions() {
await this.store.clear();
}
/**
* Get session statistics
*/
async getStats() {
if (this.store.getStats) {
return await this.store.getStats();
}
return { message: 'Statistics not available for this store type' };
}
}
// Export helper factory functions
const createSessionManager = (options = {}) => {
return new SessionManager(options);
};
const createMemoryStore = () => {
return new MemorySessionStore();
};
const createRedisStore = (redisClient, options = {}) => {
return new RedisSessionStore(redisClient, options);
};
const createDatabaseStore = (database, options = {}) => {
return new DatabaseSessionStore(database, options);
};
const createCustomHandler = (handlers) => {
return new CustomSessionHandler(handlers);
};
module.exports = {
SessionManager,
SessionStore,
MemorySessionStore,
RedisSessionStore,
DatabaseSessionStore,
CustomSessionHandler,
createSessionManager,
createMemoryStore,
createRedisStore,
createDatabaseStore,
createCustomHandler
};