Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.

Commit 976a4e3

Browse files
authored
Merge pull request #15 from ZeroHost-Code/beta
v1.0.3
2 parents 83d23cb + dba6923 commit 976a4e3

18 files changed

Lines changed: 6716 additions & 5176 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Game server management dashboard for the ZeroHost platform. Provides user-facing server lifecycle management (creation, renewal, suspension, deletion) backed by a Pyrodactyl/Pterodactyl panel API.
44

5+
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/3bf1a711-9923-4089-8a12-94bbc134c41b" />
6+
57
---
68

79
## Table of Contents

config/cap.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
const CAP_SECRET = process.env.CAP_SECRET;
22
const CAP_ENDPOINT = process.env.CAP_ENDPOINT;
33

4-
if (!CAP_SECRET) {
5-
console.error('Missing CAP_SECRET environment variable');
6-
}
7-
8-
if (!CAP_ENDPOINT) {
9-
console.error('Missing CAP_ENDPOINT environment variable');
10-
}
11-
124
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
135
const controller = new AbortController();
146
const timer = setTimeout(() => controller.abort(), timeout);

config/db.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,50 @@ const pool = mariadb.createPool({
1717
acquireTimeout: 10000,
1818
idleTimeout: 30000,
1919
insertIdAsNumber: true,
20+
pingTimeout: 5000,
2021
});
2122

23+
export async function closePool() {
24+
try {
25+
await pool.end();
26+
} catch (err) {
27+
console.error('Error closing pool:', err.message);
28+
}
29+
}
30+
31+
export async function getPoolStatus() {
32+
try {
33+
const active = pool.activeConnections();
34+
const total = pool.totalConnections();
35+
const idle = pool.idleConnections();
36+
return { active, total, idle };
37+
} catch {
38+
return { active: -1, total: -1, idle: -1 };
39+
}
40+
}
41+
2242
export async function query(sql, params = []) {
2343
let lastErr;
2444
for (let attempt = 1; attempt <= 3; attempt++) {
2545
let conn;
46+
let queryTimeout;
2647
try {
2748
conn = await pool.getConnection();
28-
const rows = await conn.query(sql, params);
49+
const timeoutPromise = new Promise((_, reject) => {
50+
queryTimeout = setTimeout(() => reject(new Error('Query timeout after 30000ms')), 30000);
51+
});
52+
let rows = await Promise.race([
53+
conn.query(sql, params),
54+
timeoutPromise,
55+
]);
56+
clearTimeout(queryTimeout);
57+
if (Array.isArray(rows) && rows.length > 10000) {
58+
console.warn(`Large result set detected: ${rows.length} rows for query: ${sql.slice(0, 100)}`);
59+
}
2960
return rows;
3061
} catch (err) {
3162
lastErr = err;
63+
clearTimeout(queryTimeout);
3264
if (err.code === 'ECONNRESET' || err.code === 'PROTOCOL_CONNECTION_LOST' || err.message?.includes('timeout')) {
3365
console.error(`DB query attempt ${attempt}/3 failed:`, err.message);
3466
if (attempt < 3) await new Promise(r => setTimeout(r, 100 * attempt));

config/migrate.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const tables = {
5656
{ name: 'id', def: 'INT AUTO_INCREMENT PRIMARY KEY' },
5757
{ name: 'ptero_nest_id', def: 'INT NOT NULL UNIQUE' },
5858
{ name: 'name', def: 'VARCHAR(255) NOT NULL' },
59+
{ name: 'logo', def: 'VARCHAR(255) DEFAULT NULL' },
60+
{ name: 'description', def: 'TEXT DEFAULT NULL' },
5961
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
6062
],
6163
},
@@ -64,6 +66,7 @@ const tables = {
6466
{ name: 'id', def: 'INT AUTO_INCREMENT PRIMARY KEY' },
6567
{ name: 'ptero_nest_id', def: 'INT NOT NULL' },
6668
{ name: 'ptero_egg_id', def: 'INT NOT NULL' },
69+
{ name: 'logo', def: 'VARCHAR(255) DEFAULT NULL' },
6770
{ name: 'cpu_limit', def: 'INT DEFAULT NULL' },
6871
{ name: 'memory_limit', def: 'INT DEFAULT NULL' },
6972
{ name: 'disk_limit', def: 'INT DEFAULT NULL' },
@@ -106,7 +109,14 @@ const constraints = [
106109
{ table: 'egg_resources', sql: 'ALTER TABLE egg_resources ADD UNIQUE INDEX idx_egg_resources_nest_egg (ptero_nest_id, ptero_egg_id)', name: 'idx_egg_resources_nest_egg' },
107110
{ table: 'notifications', sql: 'ALTER TABLE notifications ADD INDEX idx_notif_user (user_id)', name: 'idx_notif_user' },
108111
{ table: 'notifications', sql: 'ALTER TABLE notifications ADD INDEX idx_notif_user_read (user_id, is_read)', name: 'idx_notif_user_read' },
112+
{ table: 'notifications', sql: 'ALTER TABLE notifications ADD INDEX idx_notif_created (created_at)', name: 'idx_notif_created' },
109113
{ table: 'notifications', sql: 'ALTER TABLE notifications ADD CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE', name: 'fk_notif_user' },
114+
{ table: 'users', sql: 'ALTER TABLE users ADD INDEX idx_email (email)', name: 'idx_user_email' },
115+
{ table: 'users', sql: 'ALTER TABLE users ADD INDEX idx_username (username)', name: 'idx_user_username' },
116+
{ table: 'activity_log', sql: 'ALTER TABLE activity_log ADD INDEX idx_action (action)', name: 'idx_activity_action' },
117+
{ table: 'activity_log', sql: 'ALTER TABLE activity_log ADD CONSTRAINT fk_activity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE', name: 'fk_activity_user' },
118+
{ table: 'server_meta', sql: 'ALTER TABLE server_meta ADD INDEX idx_ptero_server (ptero_server_id)', name: 'idx_ptero_server' },
119+
{ table: 'server_meta', sql: 'ALTER TABLE server_meta ADD CONSTRAINT fk_server_meta_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE', name: 'fk_server_meta_user' },
110120
];
111121

112122
export async function migrate() {

config/pyrodactyl.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const PTERO_URL = process.env.PTERO_URL || 'https://panel.zero-host.org';
22
export const PTERO_API_KEY = process.env.PTERO_API_KEY || '';
3-
export const PANEL_DB_NAME = process.env.PANEL_DB_NAME || 'panel';
3+
export const PANEL_DB_NAME = (process.env.PANEL_DB_NAME || 'panel').replace(/[^a-zA-Z0-9_]/g, '');
44

55
export const SERVER_LIMITS = {
66
memory: 512,

middleware/auth.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ import { query } from '../config/db.js';
33

44
const JWT_SECRET = process.env.JWT_SECRET;
55

6-
if (!JWT_SECRET) {
7-
console.error('Missing JWT_SECRET environment variable');
8-
} else if (/[\$\(\)]/.test(JWT_SECRET)) {
9-
console.error('JWT_SECRET contains unresolved shell expansion characters ($(), backticks). Generate a proper random key (e.g. openssl rand -hex 32) and hardcode it in .env');
10-
}
11-
126
export async function authenticateToken(req, res, next) {
137
const authHeader = req.headers['authorization'];
148
const token = authHeader && authHeader.split(' ')[1];

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zerohost-dashboard",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"private": true,
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)