diff --git a/api/realtime.js b/api/realtime.js
new file mode 100644
index 0000000..f47bb00
--- /dev/null
+++ b/api/realtime.js
@@ -0,0 +1,198 @@
+const settings = require("../settings.json");
+const chalk = require('chalk');
+const fetch = require('node-fetch');
+
+module.exports.load = async function(app, db) {
+ const indexjs = require("../index.js");
+ const io = indexjs.io;
+ const statsService = indexjs.statsService;
+
+ if (!io || !statsService) {
+ console.error(chalk.red('[REALTIME] Socket.io or StatsService not initialized'));
+ return;
+ }
+ io.use(async (socket, next) => {
+ const sessionId = socket.handshake.auth.sessionId;
+ const userId = socket.handshake.auth.userId;
+ if (!userId || !sessionId) {
+ return next(new Error('Authentication required'));
+ }
+ const userExists = await db.get(`users-${userId}`);
+ if (!userExists) {
+ return next(new Error('Invalid user'));
+ }
+
+ socket.userId = userId;
+ socket.pteroUserId = userExists;
+ next();
+ });
+ io.on('connection', (socket) => {
+ console.log(chalk.cyan(`[REALTIME] User ${socket.userId} connected`));
+ socket.join(`user-${socket.userId}`);
+ socket.on('subscribe-dashboard', async () => {
+ socket.join('dashboard');
+ const allServers = statsService.getAllServersSnapshot();
+ socket.emit('dashboard-init', allServers);
+
+ console.log(chalk.green(`[REALTIME] User ${socket.userId} subscribed to dashboard`));
+ });
+ socket.on('subscribe-server', async (data) => {
+ const { identifier } = data;
+
+ if (!identifier) {
+ socket.emit('error', { message: 'Server identifier required' });
+ return;
+ }
+ const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier);
+ if (!hasAccess) {
+ socket.emit('error', { message: 'Access denied' });
+ return;
+ }
+ socket.join(`server-${identifier}`);
+ const serverData = statsService.getServerStatsSnapshot(identifier);
+ if (serverData) {
+ socket.emit('server-init', serverData);
+ }
+
+ console.log(chalk.green(`[REALTIME] User ${socket.userId} subscribed to server ${identifier}`));
+ });
+ socket.on('subscribe-console', async (data) => {
+ const { identifier } = data;
+
+ if (!identifier) {
+ socket.emit('error', { message: 'Server identifier required' });
+ return;
+ }
+ const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier);
+ if (!hasAccess) {
+ socket.emit('error', { message: 'Access denied' });
+ return;
+ }
+ const wsUrl = await getServerWebSocketUrl(identifier);
+ if (wsUrl) {
+ connectToServerConsole(socket, identifier, wsUrl);
+ } else {
+ socket.emit('error', { message: 'Could not connect to server console' });
+ }
+ });
+ socket.on('unsubscribe-dashboard', () => {
+ socket.leave('dashboard');
+ console.log(chalk.yellow(`[REALTIME] User ${socket.userId} unsubscribed from dashboard`));
+ });
+
+ socket.on('unsubscribe-server', (data) => {
+ const { identifier } = data;
+ socket.leave(`server-${identifier}`);
+ console.log(chalk.yellow(`[REALTIME] User ${socket.userId} unsubscribed from server ${identifier}`));
+ });
+ socket.on('disconnect', () => {
+ console.log(chalk.yellow(`[REALTIME] User ${socket.userId} disconnected`));
+ });
+ socket.on('request-stats-history', async (data) => {
+ const { identifier, duration } = data;
+
+ const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier);
+ if (!hasAccess) {
+ socket.emit('error', { message: 'Access denied' });
+ return;
+ }
+
+ const history = await statsService.getServerHistory(identifier, duration);
+ socket.emit('stats-history', { identifier, history });
+ });
+ });
+ async function verifyServerAccess(pteroUserId, identifier) {
+ try {
+ const response = await fetch(
+ `${settings.pterodactyl.domain}/api/client/servers/${identifier}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${settings.pterodactyl.account_key}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+ }
+ async function getServerWebSocketUrl(identifier) {
+ try {
+ const response = await fetch(
+ `${settings.pterodactyl.domain}/api/client/servers/${identifier}/websocket`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${settings.pterodactyl.account_key}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ return data.data;
+ } catch (error) {
+ return null;
+ }
+ }
+ function connectToServerConsole(clientSocket, identifier, wsData) {
+ const WebSocket = require('ws');
+ const ws = new WebSocket(wsData.socket);
+
+ ws.on('open', () => {
+ ws.send(JSON.stringify({
+ event: 'auth',
+ args: [wsData.token]
+ }));
+ ws.send(JSON.stringify({
+ event: 'send logs',
+ args: [null]
+ }));
+
+ console.log(chalk.green(`[REALTIME] Console connected for server ${identifier}`));
+ });
+
+ ws.on('message', (data) => {
+ try {
+ const message = JSON.parse(data);
+ if (message.event === 'console output') {
+ clientSocket.emit('console-output', {
+ identifier,
+ output: message.args[0]
+ });
+ }
+ if (message.event === 'status') {
+ clientSocket.emit('console-status', {
+ identifier,
+ status: message.args[0]
+ });
+ }
+ } catch (error) {
+ console.error(chalk.red('[REALTIME] Error parsing console message:'), error);
+ }
+ });
+
+ ws.on('error', (error) => {
+ console.error(chalk.red(`[REALTIME] Console WebSocket error for ${identifier}:`), error);
+ clientSocket.emit('console-error', {
+ identifier,
+ message: 'Console connection error'
+ });
+ });
+ ws.on('close', () => {
+ console.log(chalk.yellow(`[REALTIME] Console disconnected for server ${identifier}`));
+ clientSocket.emit('console-disconnected', { identifier });
+ });
+ clientSocket.on('disconnect', () => {
+ ws.close();
+ });
+ clientSocket.consoleWs = ws;
+ }
+};
diff --git a/index.js b/index.js
index 45359ca..0a8170c 100644
--- a/index.js
+++ b/index.js
@@ -118,7 +118,22 @@ app.use(express.json({
verify: undefined
}));
-const listener = app.listen(settings.website.port, function() {
+const http = require('http');
+const server = http.createServer(app);
+const { Server } = require('socket.io');
+const io = new Server(server, {
+ cors: {
+ origin: settings.api.client.oauth2.link,
+ credentials: true
+ }
+});
+
+const StatsService = require('./managers/StatsService');
+const statsService = new StatsService(settings, db);
+module.exports.io = io;
+module.exports.statsService = statsService;
+
+const listener = server.listen(settings.website.port, function() {
console.clear();
console.log(chalk.gray(" "));
console.log(chalk.gray(" ") + chalk.bgBlack(" LAPSUS CLIENT IS ONLINE "));
@@ -128,6 +143,10 @@ const listener = app.listen(settings.website.port, function() {
console.log(chalk.gray(" ") + chalk.blue("[THEME]") + chalk.white(" You're using ") + chalk.underline(settings.defaulttheme) + " theme");
console.log(chalk.gray(" "));
console.log(chalk.gray(" ") + chalk.cyan("[SYSTEM]") + chalk.white(" You can now access the dashboard at ") + chalk.underline(settings.api.client.oauth2.link + "/"));
+ console.log(chalk.gray(" "));
+ console.log(chalk.gray(" ") + chalk.magenta("[REALTIME]") + chalk.white(" WebSocket server initialized for live stats"));
+ statsService.initialize(io);
+
if (settings.defaulttheme !== 'lapsus' && settings.defaulttheme !== 'lapsusv2' && settings.defaulttheme !== 'pylex') {
console.log(chalk.gray(" "));
console.log(chalk.gray(" ") + chalk.yellow("[WARNING]") + chalk.white(" You're using an unofficial theme. This means you are exposed to vulnerabilities and bugs. Consider using the official theme or a third party theme provided by Lapsus.")); }
diff --git a/managers/StatsService.js b/managers/StatsService.js
new file mode 100644
index 0000000..937cb15
--- /dev/null
+++ b/managers/StatsService.js
@@ -0,0 +1,150 @@
+const fetch = require('node-fetch');
+const chalk = require('chalk');
+
+class StatsService {
+ constructor(settings, db) {
+ this.settings = settings;
+ this.db = db;
+ this.io = null;
+ this.updateInterval = 5000;
+ this.serverStats = new Map();
+ this.intervalId = null;
+ }
+
+ initialize(io) {
+ this.io = io;
+ console.log(chalk.green('[STATS] Real-time stats service initialized'));
+ this.startPolling();
+ }
+
+ startPolling() {
+ if (this.intervalId) return;
+ this.intervalId = setInterval(async () => {
+ await this.pollAllServers();
+ }, this.updateInterval);
+ this.pollAllServers();
+ }
+
+ stopPolling() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ }
+
+ async pollAllServers() {
+ try {
+ const allServersResponse = await fetch(
+ `${this.settings.pterodactyl.domain}/api/application/servers?per_page=100`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.settings.pterodactyl.key}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (!allServersResponse.ok) {
+ console.error(chalk.red('[STATS] Failed to fetch servers from Pterodactyl'));
+ return;
+ }
+
+ const serversData = await allServersResponse.json();
+ const servers = serversData.data || [];
+
+ for (const server of servers) {
+ const stats = await this.getServerStats(server.attributes.identifier);
+
+ if (stats) {
+ const serverData = {
+ serverId: server.attributes.id,
+ identifier: server.attributes.identifier,
+ name: server.attributes.name,
+ userId: server.attributes.user,
+ status: stats.current_state || 'offline',
+ resources: stats.resources || {},
+ timestamp: Date.now()
+ };
+
+ this.serverStats.set(server.attributes.identifier, serverData);
+ this.io.to(`server-${server.attributes.identifier}`).emit('server-stats', serverData);
+ this.io.to('dashboard').emit('server-update', serverData);
+ }
+ }
+ } catch (error) {
+ console.error(chalk.red('[STATS] Error polling servers:'), error.message);
+ }
+ }
+
+ async getAllUserKeys() {
+ try {
+ const keys = [];
+ return keys;
+ } catch (error) {
+ return [];
+ }
+ }
+
+ async getUserServers(pteroUserId) {
+ try {
+ const response = await fetch(
+ `${this.settings.pterodactyl.domain}/api/application/users/${pteroUserId}?include=servers`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.settings.pterodactyl.key}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ return data.attributes?.relationships?.servers?.data || [];
+ } catch (error) {
+ console.error(chalk.red('[STATS] Error fetching user servers:'), error.message);
+ return null;
+ }
+ }
+
+ async getServerStats(identifier) {
+ try {
+ const response = await fetch(
+ `${this.settings.pterodactyl.domain}/api/client/servers/${identifier}/resources`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.settings.pterodactyl.account_key}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ return data.attributes || null;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ getServerStatsSnapshot(identifier) {
+ return this.serverStats.get(identifier);
+ }
+
+ getAllServersSnapshot() {
+ return Array.from(this.serverStats.values());
+ }
+
+ async getServerHistory(identifier, duration = 3600000) { // 1 hour default
+ return [this.serverStats.get(identifier)];
+ }
+}
+
+module.exports = StatsService;
diff --git a/package-lock.json b/package-lock.json
index 4ef5fed..90fdeea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,9 +15,10 @@
"@keyv/redis": "^2.2.1",
"@keyv/sqlite": "^3.5.2",
"@tailwindcss/forms": "^0.5.3",
- "axios": "^1.9.0",
+ "axios": "^1.12.2",
"body-parser": "^1.20.3",
"chalk": "^4.1.0",
+ "chart.js": "^4.5.1",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"credit-card-validate": "^0.9.3",
@@ -41,11 +42,13 @@
"passport-google-oauth20": "^2.0.0",
"path": "^0.12.7",
"smtp-server": "^3.13.5",
+ "socket.io": "^4.8.1",
"stripe": "^9.4.0",
"systeminformation": "^5.25.11",
"tailwindcss": "^3.3.1",
"unzipper": "^0.12.3",
- "warn": "^1.0.1"
+ "warn": "^1.0.1",
+ "ws": "^8.18.3"
}
},
"node_modules/@alloc/quick-lru": {
@@ -302,6 +305,12 @@
"node": ">= 14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
@@ -408,6 +417,12 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@@ -430,6 +445,15 @@
"node": ">= 6"
}
},
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -742,9 +766,9 @@
}
},
"node_modules/axios": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
- "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -787,6 +811,15 @@
],
"license": "MIT"
},
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
@@ -1079,6 +1112,18 @@
"node": "*"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1522,6 +1567,27 @@
"node": ">=12.0.0"
}
},
+ "node_modules/discord.js/node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -1731,6 +1797,88 @@
"once": "^1.4.0"
}
},
+ "node_modules/engine.io": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/engine.io/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -5486,6 +5634,137 @@
"node": ">=6.0.0"
}
},
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-adapter/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
@@ -6467,16 +6746,16 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "7.5.10",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
- "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
- "node": ">=8.3.0"
+ "node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
+ "utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
diff --git a/package.json b/package.json
index 8318f00..4f05afd 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,10 @@
"@keyv/redis": "^2.2.1",
"@keyv/sqlite": "^3.5.2",
"@tailwindcss/forms": "^0.5.3",
- "axios": "^1.9.0",
+ "axios": "^1.12.2",
"body-parser": "^1.20.3",
"chalk": "^4.1.0",
+ "chart.js": "^4.5.1",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"credit-card-validate": "^0.9.3",
@@ -38,11 +39,13 @@
"passport-google-oauth20": "^2.0.0",
"path": "^0.12.7",
"smtp-server": "^3.13.5",
+ "socket.io": "^4.8.1",
"stripe": "^9.4.0",
"systeminformation": "^5.25.11",
"tailwindcss": "^3.3.1",
"unzipper": "^0.12.3",
- "warn": "^1.0.1"
+ "warn": "^1.0.1",
+ "ws": "^8.18.3"
},
"scripts": {
"start": "nodemon index.js",
diff --git a/themes/lapsus/components/navigation.ejs b/themes/lapsus/components/navigation.ejs
index 9510602..9f4c6c9 100644
--- a/themes/lapsus/components/navigation.ejs
+++ b/themes/lapsus/components/navigation.ejs
@@ -81,6 +81,13 @@ font-weight: semibold;
Panel
+
+
+ Real-Time Monitor
+
+
diff --git a/themes/lapsus/pages.json b/themes/lapsus/pages.json
index 8ae848e..3e1a7af 100644
--- a/themes/lapsus/pages.json
+++ b/themes/lapsus/pages.json
@@ -69,7 +69,8 @@
"servers": "servers.ejs",
"lv": "lv.ejs",
"j4r": "j4r.ejs",
- "themes": "themes.ejs"
+ "themes": "themes.ejs",
+ "realtime": "realtime-dashboard.ejs"
},
"mustbeloggedin": [
"/dashboard",
@@ -82,7 +83,8 @@
"/redeem",
"/servers",
"/store",
- "/earn"
+ "/earn",
+ "/realtime"
],
"mustbeadmin": [
"/admin",
diff --git a/themes/lapsus/realtime-dashboard.ejs b/themes/lapsus/realtime-dashboard.ejs
new file mode 100644
index 0000000..3893f39
--- /dev/null
+++ b/themes/lapsus/realtime-dashboard.ejs
@@ -0,0 +1,431 @@
+
+
Real-Time Dashboard - <%= settings.name %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%- include('components/navigation') %>
+
+
+
+
+
+
+
+
+
+
+ Real-Time Server Monitor
+
+
+ Live statistics and console streaming for all your servers
+
+
+
+
+ Connection:
+ ● Connected
+
+
+
+
+
+
+
+
+
+
+
+
+ - Total Servers
+ - 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
No servers found
+
Connect to WebSocket to see your servers in real-time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Live Console
+
+
+
+
Connecting to server console...
+
+
+
+
+
+
+