Skip to content
Merged

Beta #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cp .env /tmp/beta.env 2>/dev/null; cp port.txt /tmp/beta.port.txt 2>/dev/null
git fetch origin --prune
git checkout beta
git merge --ff-only origin/beta
git reset --hard origin/beta
cp /tmp/beta.env .env 2>/dev/null; cp /tmp/beta.port.txt port.txt 2>/dev/null
grep 'discord.zero-host.org' public/js/app.js || echo "⚠ DISCORD LINK NOT FOUND IN FILE"
head -20 port.txt 2>/dev/null || echo "⚠ port.txt missing or empty"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cp .env /tmp/main.env 2>/dev/null; cp port.txt /tmp/main.port.txt 2>/dev/null
git fetch origin --prune
git checkout main
git merge --ff-only origin/main
git reset --hard origin/main
cp /tmp/main.env .env 2>/dev/null; cp /tmp/main.port.txt port.txt 2>/dev/null
npm install
pm2 restart zerohost-dashboard
155 changes: 144 additions & 11 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1514,21 +1514,25 @@ function renderAccountEdit() {
Add your Pyrodactyl Client API key to enable live resource monitoring and server power state detection directly in the dashboard.
Generate one at <a href="https://panel.zero-host.org/account/api" target="_blank">panel.zero-host.org/account/api</a>.
</p>
<form id="api-key-form" style="max-width:480px">
<div class="form-group">
<label for="ptero-api-key-input">API Key</label>
<input type="password" id="ptero-api-key-input" placeholder="ptla_..." autocomplete="off" />
</div>
<button type="submit" class="btn btn-primary btn-full" id="save-api-key-btn">Save</button>
</form>
<div id="api-key-status" style="margin-top:8px;font-size:0.82rem;color:var(--text-muted)"></div>
<div id="api-key-section-content">
<form id="api-key-form" style="max-width:480px">
<div class="form-group">
<label for="ptero-api-key-input">API Key</label>
<input type="password" id="ptero-api-key-input" placeholder="ptla_..." autocomplete="off" />
</div>
<button type="submit" class="btn btn-primary btn-full" id="save-api-key-btn">Save</button>
</form>
<div id="api-key-status" style="margin-top:8px;font-size:0.82rem;color:var(--text-muted)"></div>
</div>
</div>
</div>
`;

$('#change-email-form').addEventListener('submit', handleChangeEmail);
$('#change-password-form').addEventListener('submit', handleChangePassword);
$('#api-key-form').addEventListener('submit', handleSaveApiKey);

checkApiKeyStatus();
}

async function handleSaveApiKey(e) {
Expand All @@ -1553,10 +1557,9 @@ async function handleSaveApiKey(e) {
method: 'PUT',
body: JSON.stringify({ apiKey: key }),
});
status.textContent = 'API key saved successfully!';
status.style.color = 'var(--accent-green)';
input.value = '';
showToast('Pyrodactyl API key saved', 'success');
renderApiKeySaved();
return;
} catch (err) {
status.textContent = err.message;
status.style.color = 'var(--accent-red)';
Expand All @@ -1566,6 +1569,136 @@ async function handleSaveApiKey(e) {
}
}

async function checkApiKeyStatus() {
try {
const data = await api('/servers/client-api-key');
if (data.hasKey) {
renderApiKeySaved();
}
} catch (err) {
// Keep default form if check fails
}
}

function renderApiKeySaved() {
const section = $('#api-key-section-content');
section.innerHTML = html`
<div style="padding:8px 0">
<p style="color:var(--accent-green);font-size:0.9rem;margin-bottom:16px">
&#10003; Your Pyrodactyl API key is saved and active.
</p>
<div style="display:flex;gap:8px">
<button class="btn btn-primary" id="modify-api-key-btn">Modify</button>
<button class="btn btn-danger" id="delete-api-key-btn">Delete</button>
</div>
</div>
`;
$('#modify-api-key-btn').addEventListener('click', handleModifyApiKey);
$('#delete-api-key-btn').addEventListener('click', handleDeleteApiKey);
}

function renderApiKeyForm() {
const section = $('#api-key-section-content');
section.innerHTML = html`
<form id="api-key-form" style="max-width:480px">
<div class="form-group">
<label for="ptero-api-key-input">API Key</label>
<input type="password" id="ptero-api-key-input" placeholder="ptla_..." autocomplete="off" />
</div>
<button type="submit" class="btn btn-primary btn-full" id="save-api-key-btn">Save</button>
</form>
<div id="api-key-status" style="margin-top:8px;font-size:0.82rem;color:var(--text-muted)"></div>
`;
$('#api-key-form').addEventListener('submit', handleSaveApiKey);
}

function handleModifyApiKey() {
const overlay = $('#modal-overlay');
const content = $('#modal-content');
content.innerHTML = html`
<div class="modal-title">Modify API Key</div>
<p style="color:var(--text-secondary);line-height:1.6;margin-bottom:16px">
Enter your new Pyrodactyl API key. The old key will be replaced.
</p>
<div class="form-group">
<label for="modal-api-key-input">New API Key</label>
<input type="password" id="modal-api-key-input" placeholder="ptla_..." autocomplete="off" />
</div>
<div class="modal-actions">
<button class="btn btn-ghost btn-full modal-cancel-btn">Cancel</button>
<button class="btn btn-primary btn-full" id="confirm-modify-api-key-btn">Save</button>
</div>
`;
overlay.classList.add('open');
$('#confirm-modify-api-key-btn').addEventListener('click', handleConfirmModifyApiKey);
}

function handleDeleteApiKey() {
const overlay = $('#modal-overlay');
const content = $('#modal-content');
content.innerHTML = html`
<div class="modal-title">Delete API Key</div>
<p style="color:var(--text-secondary);line-height:1.6;margin-bottom:16px">
Are you sure you want to delete your Pyrodactyl API key? Live resource monitoring and power state detection will stop working.
</p>
<div class="modal-actions">
<button class="btn btn-ghost btn-full modal-cancel-btn">Cancel</button>
<button class="btn btn-danger btn-full" id="confirm-delete-api-key-btn">Delete</button>
</div>
`;
overlay.classList.add('open');
$('#confirm-delete-api-key-btn').addEventListener('click', handleConfirmDeleteApiKey);
}

async function handleConfirmModifyApiKey() {
const btn = $('#confirm-modify-api-key-btn');
const input = $('#modal-api-key-input');
const key = input.value.trim();

if (!key) {
showToast('Please enter an API key', 'error');
return;
}

btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>';

try {
await api('/servers/client-api-key', {
method: 'PUT',
body: JSON.stringify({ apiKey: key }),
});
$('#modal-overlay').classList.remove('open');
showToast('Pyrodactyl API key updated', 'success');
renderApiKeySaved();
} catch (err) {
showToast(err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = 'Save';
}
}

async function handleConfirmDeleteApiKey() {
const btn = $('#confirm-delete-api-key-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>';

try {
await api('/servers/client-api-key', {
method: 'DELETE',
});
$('#modal-overlay').classList.remove('open');
showToast('Pyrodactyl API key deleted', 'success');
renderApiKeyForm();
} catch (err) {
showToast(err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = 'Delete';
}
}

function renderAccountLinks() {
const el = $('#page-account');
el.innerHTML = html`
Expand Down
24 changes: 24 additions & 0 deletions routes/servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,18 @@ router.post('/power/:identifier', authenticateToken, async (req, res) => {
}
});

router.get('/client-api-key', authenticateToken, async (req, res) => {
try {
const userId = req.user.userId;
const rows = await query('SELECT ptero_client_api_key FROM users WHERE id = ?', [userId]);
const hasKey = rows.length > 0 && rows[0].ptero_client_api_key !== null;
res.json({ hasKey });
} catch (err) {
console.error('API key check error:', err.message);
res.status(500).json({ error: 'Failed to check API key status' });
}
});

router.put('/client-api-key', authenticateToken, async (req, res) => {
try {
const { apiKey } = req.body;
Expand All @@ -469,4 +481,16 @@ router.put('/client-api-key', authenticateToken, async (req, res) => {
}
});

router.delete('/client-api-key', authenticateToken, async (req, res) => {
try {
const userId = req.user.userId;
await query('UPDATE users SET ptero_client_api_key = NULL WHERE id = ?', [userId]);
await logActivity(req.user.userId, 'api_key_deleted', 'Deleted Pyrodactyl API key');
res.json({ success: true });
} catch (err) {
console.error('API key delete error:', err.message);
res.status(500).json({ error: 'Failed to delete API key' });
}
});

export default router;