Skip to content

Commit d9f8be4

Browse files
authored
Merge pull request #30 from ZeroHost-Code/main
sync
2 parents cc9b689 + 7952475 commit d9f8be4

8 files changed

Lines changed: 108 additions & 23 deletions

File tree

.github/workflows/deploy-beta.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
branches: [beta]
66

7+
permissions:
8+
contents: read
9+
710
jobs:
811
deploy:
912
runs-on: ubuntu-latest

.github/workflows/deploy-main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
branches: [main]
66

7+
permissions:
8+
contents: read
9+
710
jobs:
811
deploy:
912
runs-on: ubuntu-latest

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

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

5-
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/403b7c93-1121-4f4c-b407-ed1235b73218" />
5+
> [!NOTE]
6+
> The dashboard is now in "cruise mode", instead of dozens of commits a day, expect around 2-3 commits per day going forward, as I'm juggling other projects on the side. That said, there will still be days with bigger pushes when needed.
67
7-
[![ZHSL](https://img.zero-host.org/assets/github.png)](https://github.com/ZeroHost-Code/legal/blob/main/zhsl.md)
8+
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/403b7c93-1121-4f4c-b407-ed1235b73218" />
89

910
---
1011

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.8",
3+
"version": "1.0.9",
44
"private": true,
55
"type": "module",
66
"scripts": {

public/js/admin.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -501,17 +501,23 @@ function updateAdminNav() {
501501
}
502502

503503
let adminServersPage = 1;
504+
let adminServersSearch = '';
504505

505506
async function renderAdminServers() {
506507
document.querySelectorAll('.admin-page').forEach(p => p.classList.remove('active'));
507508
const el = $a('#admin-page-servers');
508509
if (!el) return;
509510
el.classList.add('active');
511+
adminServersSearch = '';
510512
el.innerHTML = ahtml`
511513
<div class="page-header">
512514
<h1 class="page-title">All Servers</h1>
513515
<p class="page-subtitle">All servers across all users</p>
514516
</div>
517+
<div class="search-wrapper" style="margin-bottom:16px;max-width:400px" id="admin-servers-search-wrapper">
518+
<span data-lucide="search"></span>
519+
<input type="text" id="admin-servers-search" placeholder="Search by server name or owner...">
520+
</div>
515521
<div class="table-container">
516522
<table>
517523
<thead>
@@ -531,6 +537,19 @@ async function renderAdminServers() {
531537
</div>
532538
`;
533539

540+
const searchInput = $a('#admin-servers-search');
541+
if (searchInput) {
542+
let searchTimer;
543+
searchInput.addEventListener('input', () => {
544+
clearTimeout(searchTimer);
545+
searchTimer = setTimeout(() => {
546+
adminServersSearch = searchInput.value.trim();
547+
adminServersPage = 1;
548+
fetchAdminServers(1);
549+
}, 400);
550+
});
551+
}
552+
534553
await fetchAdminServers(adminServersPage);
535554
}
536555

@@ -539,9 +558,11 @@ async function fetchAdminServers(pageNum) {
539558
const limit = 10;
540559
const offset = (pageNum - 1) * limit;
541560
const paginationEl = $a('#admin-servers-pagination');
561+
const params = new URLSearchParams({ limit, offset });
562+
if (adminServersSearch) params.set('search', adminServersSearch);
542563

543564
try {
544-
const data = await adminApi(`/servers?limit=${limit}&offset=${offset}`);
565+
const data = await adminApi(`/servers?${params.toString()}`);
545566
const tbody = $a('#admin-servers-tbody');
546567
if (!tbody) return;
547568

@@ -589,7 +610,7 @@ async function fetchAdminServers(pageNum) {
589610
}
590611
} catch (err) {
591612
const tbody = $a('#admin-servers-tbody');
592-
if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${err.message}</td></tr>`;
613+
if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${escapeHtml(err.message)}</td></tr>`;
593614
}
594615
initIcons();
595616
}
@@ -1151,7 +1172,7 @@ async function renderAdminDashboard() {
11511172
});
11521173
}
11531174
} catch (err) {
1154-
el.innerHTML = `<div class="empty-state"><div class="empty-state-title">Error</div><div class="empty-state-desc">${err.message}</div></div>`;
1175+
el.innerHTML = `<div class="empty-state"><div class="empty-state-title">Error</div><div class="empty-state-desc">${escapeHtml(err.message)}</div></div>`;
11551176
}
11561177
}
11571178

@@ -1212,7 +1233,7 @@ async function renderAdminUsers() {
12121233
`).join('');
12131234
} catch (err) {
12141235
const tbody = $a('#admin-users-tbody');
1215-
if (tbody) tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${err.message}</td></tr>`;
1236+
if (tbody) tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${escapeHtml(err.message)}</td></tr>`;
12161237
}
12171238
initIcons();
12181239
}
@@ -1639,7 +1660,7 @@ async function renderAdminEggsSettings() {
16391660
});
16401661
} catch (err) {
16411662
const tbody = $a('#admin-nests-tbody');
1642-
if (tbody) tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${err.message}</td></tr>`;
1663+
if (tbody) tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${escapeHtml(err.message)}</td></tr>`;
16431664
}
16441665
}
16451666

@@ -1737,7 +1758,7 @@ async function renderAdminNestEggs(nestId) {
17371758
});
17381759
} catch (err) {
17391760
const tbody = $a('#admin-eggs-tbody');
1740-
if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${err.message}</td></tr>`;
1761+
if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${escapeHtml(err.message)}</td></tr>`;
17411762
}
17421763
initIcons();
17431764
}
@@ -1870,7 +1891,9 @@ async function renderAdminEggSettings(nestId, eggId) {
18701891
const val = $a('#egg-logo').value;
18711892
if (val) {
18721893
if (preview) preview.style.display = 'block';
1873-
if (img) img.src = val;
1894+
if (img) {
1895+
try { const u = new URL(val); if (u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'data:' || u.protocol === 'blob:') img.src = u.href; else img.src = ''; } catch { img.src = ''; }
1896+
}
18741897
} else {
18751898
if (preview) preview.style.display = 'none';
18761899
}
@@ -2228,7 +2251,9 @@ function showRenameNestModal(nestId, currentName, currentLogo, currentDescriptio
22282251
const img = preview?.querySelector('img');
22292252
if (logoInput.value) {
22302253
if (preview) preview.style.display = 'block';
2231-
if (img) img.src = logoInput.value;
2254+
if (img) {
2255+
try { const u = new URL(logoInput.value); if (u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'data:' || u.protocol === 'blob:') img.src = u.href; else img.src = ''; } catch { img.src = ''; }
2256+
}
22322257
} else {
22332258
if (preview) preview.style.display = 'none';
22342259
}
@@ -2381,7 +2406,7 @@ async function renderAdminNodes() {
23812406
}).join('');
23822407
} catch (err) {
23832408
const tbody = $a('#admin-nodes-tbody');
2384-
if (tbody) tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${err.message}</td></tr>`;
2409+
if (tbody) tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${escapeHtml(err.message)}</td></tr>`;
23852410
}
23862411
}
23872412

public/js/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ async function renderDashboard() {
14961496
<div style="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">
14971497
14981498
</div>
1499-
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.0.8</div>
1499+
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.0.9</div>
15001500
</div>
15011501
<div class="sidebar-resizer" id="sidebar-resizer"></div>
15021502
</aside>
@@ -3296,7 +3296,7 @@ async function loadPasskeys() {
32963296
initIcons();
32973297
} catch (err) {
32983298
const list = $('#passkey-list');
3299-
if (list) list.innerHTML = `<p style="color:var(--accent-red);font-size:0.85rem">Failed to load passkeys: ${err.message}</p>`;
3299+
if (list) list.innerHTML = `<p style="color:var(--accent-red);font-size:0.85rem">Failed to load passkeys: ${escapeHtml(err.message)}</p>`;
33003300
}
33013301
}
33023302

routes/admin.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,24 @@ router.get('/servers', authenticateToken, requireAdmin, async (req, res) => {
138138
try {
139139
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
140140
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
141+
const search = (req.query.search || '').trim().toLowerCase();
141142

142-
const result = await getAllServers(limit, offset);
143-
const { servers: paginatedServers, total } = result;
144-
const page = Math.floor(offset / limit) + 1;
145-
const totalPages = Math.ceil(total / limit) || 1;
143+
let allServers;
144+
if (search) {
145+
const result = await getAllServers();
146+
allServers = Array.isArray(result) ? result : result.servers || [];
147+
} else {
148+
const result = await getAllServers(limit, offset);
149+
allServers = result.servers || [];
150+
}
146151

147152
const users = await query('SELECT id, email, username, ptero_user_id FROM users');
148153
const userMap = {};
149154
for (const u of users) {
150155
userMap[u.ptero_user_id] = { id: u.id, email: u.email, username: u.username };
151156
}
152157

153-
for (const s of paginatedServers) {
158+
for (const s of allServers) {
154159
s.owner = userMap[s.user] || { id: null, email: 'Unknown', username: 'Unknown' };
155160
try {
156161
const meta = await query('SELECT * FROM server_meta WHERE ptero_server_id = ?', [s.id]);
@@ -160,7 +165,22 @@ router.get('/servers', authenticateToken, requireAdmin, async (req, res) => {
160165
}
161166
}
162167

163-
res.json({ servers: paginatedServers, total, page, totalPages, limit });
168+
let filtered = allServers;
169+
if (search) {
170+
filtered = allServers.filter(s => {
171+
const name = (s.name || '').toLowerCase();
172+
const ownerName = (s.owner?.username || '').toLowerCase();
173+
const ownerEmail = (s.owner?.email || '').toLowerCase();
174+
return name.includes(search) || ownerName.includes(search) || ownerEmail.includes(search);
175+
});
176+
}
177+
178+
const filteredTotal = filtered.length;
179+
const page = Math.floor(offset / limit) + 1;
180+
const totalPages = Math.ceil(filteredTotal / limit) || 1;
181+
const sliced = filtered.slice(offset, offset + limit);
182+
183+
res.json({ servers: sliced, total: filteredTotal, page, totalPages, limit });
164184
} catch (err) {
165185
console.error('Admin servers list error:', err.message);
166186
res.status(500).json({ error: 'Failed to fetch servers' });

server.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,30 @@ app.use((err, req, res, next) => {
162162
});
163163
app.use(cookieParser(process.env.COOKIE_SECRET));
164164

165+
const csrfExemptPaths = ['/api/auth/login', '/api/auth/register', '/api/auth/passkey/options', '/api/auth/passkey/verify'];
166+
app.use((req, res, next) => {
167+
if (!req.path.startsWith('/api/')) return next();
168+
if (csrfExemptPaths.includes(req.path)) return next();
169+
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
170+
const token = crypto.randomBytes(32).toString('hex');
171+
if (!req.cookies['XSRF-TOKEN']) {
172+
res.cookie('XSRF-TOKEN', token, {
173+
sameSite: 'strict',
174+
secure: process.env.NODE_ENV === 'production',
175+
httpOnly: false,
176+
maxAge: 24 * 60 * 60 * 1000,
177+
});
178+
}
179+
return next();
180+
}
181+
const headerToken = req.headers['x-csrf-token'];
182+
const cookieToken = req.cookies['XSRF-TOKEN'];
183+
if (!headerToken || !cookieToken || headerToken !== cookieToken) {
184+
return res.status(403).json({ error: 'Invalid CSRF token', requestId: req.requestId });
185+
}
186+
next();
187+
});
188+
165189
app.use((req, res, next) => {
166190
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
167191
res.setHeader('X-Robots-Tag', 'noindex, nofollow');
@@ -197,6 +221,15 @@ const activityLimiter = rateLimit({
197221
trustProxy: trustProxy ? 1 : 0,
198222
});
199223

224+
const staticLimiter = rateLimit({
225+
windowMs: 60 * 1000,
226+
max: 60,
227+
message: { error: 'Too many requests' },
228+
standardHeaders: true,
229+
legacyHeaders: false,
230+
trustProxy: trustProxy ? 1 : 0,
231+
});
232+
200233
app.use('/api/auth/login', authLimiter);
201234
app.use('/api/auth/register', authLimiter);
202235
app.use('/api/activity', activityLimiter);
@@ -287,17 +320,17 @@ app.get('/api/health', async (req, res) => {
287320

288321
app.use(express.static(path.join(__dirname, 'public')));
289322

290-
app.get('/admin/*', (req, res) => {
323+
app.get('/admin/*', staticLimiter, (req, res) => {
291324
res.set('X-Robots-Tag', 'noindex, nofollow');
292325
res.sendFile(path.join(__dirname, 'public', 'index.html'));
293326
});
294327

295-
app.get('/admin', (req, res) => {
328+
app.get('/admin', staticLimiter, (req, res) => {
296329
res.set('X-Robots-Tag', 'noindex, nofollow');
297330
res.sendFile(path.join(__dirname, 'public', 'index.html'));
298331
});
299332

300-
app.get('*', (req, res) => {
333+
app.get('*', staticLimiter, (req, res) => {
301334
if (req.path.startsWith('/api/')) {
302335
return res.status(404).json({ error: 'Not found' });
303336
}

0 commit comments

Comments
 (0)