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
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Online
+
0
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Offline
+
0
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Avg CPU
+
0%
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + + +