Skip to content

Commit 05b8c4e

Browse files
committed
feat: auto-login to Pyrodactyl via one-time token
- New GET /api/servers/auto-login endpoint that generates a one-time login token using the user's Client API key - Opens Pyrodactyl panel with auto-login token + redirect to specific server - Falls back to direct panel URL if no API key is configured - Updated all Open Panel / Manage Pyrodactyl buttons to use auto-login - Updated /pyrodactyl page for auto-login flow
1 parent bc59cd2 commit 05b8c4e

2 files changed

Lines changed: 51 additions & 28 deletions

File tree

public/js/app.js

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ function renderCookieBanner() {
4545
});
4646
}
4747

48+
async function openPyrodactylPanel(serverIdentifier) {
49+
const data = await api('/servers/auto-login');
50+
let url = data.autoLogin
51+
? `${data.panelUrl}/auth/login?token=${data.token}${serverIdentifier ? `&redirect=${encodeURIComponent('/server/' + serverIdentifier)}` : ''}`
52+
: `${data.panelUrl}${serverIdentifier ? '/server/' + serverIdentifier : ''}`;
53+
window.open(url, '_blank');
54+
}
55+
4856
function $(sel) { return document.querySelector(sel); }
4957
function md5(s) {
5058
function F(x,y,z) { return (x & y) | (~x & z); }
@@ -969,10 +977,10 @@ function renderServerCard(s) {
969977
</div>
970978
` : ''}
971979
<div class="server-card-actions">
972-
<a href="https://panel.zero-host.org/server/${s.identifier}" target="_blank" class="btn btn-ghost btn-sm">
980+
<button class="btn btn-ghost btn-sm" onclick="openPyrodactylPanel('${s.identifier}')">
973981
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3"/></svg>
974982
Open Panel
975-
</a>
983+
</button>
976984
${canRenew ? html`
977985
<button class="btn btn-primary btn-sm btn-renew-server" data-server-id="${s.id}">Renew</button>
978986
` : ''}
@@ -1004,7 +1012,7 @@ function renderServerRow(s) {
10041012
<td>
10051013
<div style="display:flex;gap:6px">
10061014
<a class="btn btn-ghost btn-sm" href="/server/${s.id}" onclick="event.preventDefault();navigateTo('server/${s.id}')">Settings</a>
1007-
<a href="https://panel.zero-host.org/server/${s.identifier}" target="_blank" class="btn btn-ghost btn-sm">Open Pyrodactyl</a>
1015+
<button class="btn btn-ghost btn-sm" onclick="openPyrodactylPanel('${s.identifier}')">Manage Pyrodactyl</button>
10081016
${canRenew ? html`
10091017
<button class="btn btn-primary btn-sm btn-renew-server" data-server-id="${s.id}">Renew</button>
10101018
` : ''}
@@ -1277,15 +1285,12 @@ async function handleCreateServer(e) {
12771285
}
12781286

12791287
// ===== PYRODACTYL PAGE =====
1280-
let pteroTimeout = null;
1281-
12821288
function renderPyrodactyl() {
1283-
if (pteroTimeout) clearTimeout(pteroTimeout);
12841289
const el = $('#page-pyrodactyl');
12851290
el.innerHTML = html`
12861291
<div class="page-header">
12871292
<h1 class="page-title">Pyrodactyl Panel</h1>
1288-
<p class="page-subtitle">Redirecting to the panel in 5 seconds...</p>
1293+
<p class="page-subtitle">Opening Pyrodactyl with auto-login...</p>
12891294
</div>
12901295
<div class="ptero-grid">
12911296
<div class="card ptero-card">
@@ -1294,32 +1299,16 @@ function renderPyrodactyl() {
12941299
</div>
12951300
<h2 class="ptero-card-title">Opening Pyrodactyl...</h2>
12961301
<p class="ptero-card-desc">
1297-
You are being redirected to the Pyrodactyl panel. If nothing happens, click the button below.
1302+
You are being automatically logged in to the Pyrodactyl panel. If nothing happens, click the button below.
12981303
</p>
1299-
<div class="ptero-info" style="margin-bottom:24px">
1300-
<div class="ptero-info-item">
1301-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
1302-
<span><strong>Login:</strong> use your dashboard email and password</span>
1303-
</div>
1304-
<div class="ptero-info-item">
1305-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
1306-
<span><strong>Same password</strong> as your dashboard account</span>
1307-
</div>
1308-
</div>
1309-
<a href="https://panel.zero-host.org" target="_blank" class="btn btn-primary btn-full" id="ptero-open-btn">
1304+
<button class="btn btn-primary btn-full" id="ptero-open-btn" onclick="openPyrodactylPanel()">
13101305
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3"/></svg>
13111306
Open Panel Now
1312-
</a>
1307+
</button>
13131308
</div>
13141309
</div>
13151310
`;
1316-
pteroTimeout = setTimeout(() => {
1317-
window.open('https://panel.zero-host.org', '_blank');
1318-
}, 5000);
1319-
document.getElementById('ptero-open-btn').addEventListener('click', () => {
1320-
clearTimeout(pteroTimeout);
1321-
pteroTimeout = null;
1322-
});
1311+
setTimeout(() => openPyrodactylPanel(), 500);
13231312
}
13241313

13251314
// ===== ACCOUNT PAGE =====
@@ -1825,7 +1814,7 @@ async function renderServerDetail(serverId) {
18251814
<p class="action-card-desc">Access the full Pyrodactyl control panel to manage files, console, databases, schedules, and more.</p>
18261815
</div>
18271816
</div>
1828-
<a href="https://panel.zero-host.org/server/${s.identifier}" target="_blank" class="btn btn-primary btn-full">Open Panel</a>
1817+
<button class="btn btn-primary btn-full" onclick="openPyrodactylPanel('${s.identifier}')">Open Panel</button>
18291818
</div>
18301819
18311820
<div class="action-card">

routes/servers.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,40 @@ router.get('/resources/:identifier', authenticateToken, async (req, res) => {
390390
}
391391
});
392392

393+
router.get('/auto-login', authenticateToken, async (req, res) => {
394+
try {
395+
const userId = req.user.userId;
396+
const users = await query('SELECT ptero_client_api_key FROM users WHERE id = ?', [userId]);
397+
398+
if (!users[0]?.ptero_client_api_key) {
399+
return res.json({ autoLogin: false, panelUrl: PTERO_URL });
400+
}
401+
402+
const apiKey = users[0].ptero_client_api_key;
403+
const pteroRes = await fetch(`${PTERO_URL}/api/client/account/one-time-token`, {
404+
method: 'POST',
405+
headers: {
406+
'Authorization': `Bearer ${apiKey}`,
407+
'Accept': 'application/json',
408+
'Content-Type': 'application/json',
409+
},
410+
signal: AbortSignal.timeout(10000),
411+
});
412+
413+
if (!pteroRes.ok) {
414+
return res.json({ autoLogin: false, panelUrl: PTERO_URL });
415+
}
416+
417+
const data = await pteroRes.json();
418+
const token = data.attributes?.token || data.token;
419+
420+
res.json({ autoLogin: true, token, panelUrl: PTERO_URL });
421+
} catch (err) {
422+
console.error('Auto-login error:', err.message);
423+
res.status(500).json({ error: 'Failed to generate auto-login link' });
424+
}
425+
});
426+
393427
router.put('/client-api-key', authenticateToken, async (req, res) => {
394428
try {
395429
const { apiKey } = req.body;

0 commit comments

Comments
 (0)