Skip to content

Commit 05fd5e1

Browse files
authored
Merge pull request #50 from ZeroHost-Code/beta
v1.2.8
2 parents 6916348 + e3e8eb9 commit 05fd5e1

8 files changed

Lines changed: 115 additions & 24 deletions

File tree

middleware/security.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function vpnProxyProtection() {
126126
}
127127
if (blacklistResult.listed) {
128128
recordFailedAction(ip);
129+
console.warn(`[DNSBL] IP ${cleanIp} blacklisted on: ${blacklistResult.blacklists.join(', ')}`);
129130
return res.status(403).json({
130131
error: 'Access denied. Your IP has been flagged.',
131132
requestId: req.requestId,

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

public/css/style.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,6 +3688,7 @@ tbody tr:hover {
36883688
.activity-icon-passkey_registered { background: rgba(6,182,212,0.15); color: #06b6d4; }
36893689
.activity-icon-email_verified { background: rgba(5,150,105,0.15); color: #059669; }
36903690
.activity-icon-passkey_deleted { background: rgba(239,68,68,0.15); color: #ef4444; }
3691+
.activity-icon-passkey_renamed { background: rgba(245,158,11,0.15); color: #f59e0b; }
36913692
.activity-icon-totp_enabled { background: rgba(5,150,105,0.15); color: #059669; }
36923693
.activity-icon-totp_disabled { background: rgba(239,68,68,0.15); color: #ef4444; }
36933694
.activity-icon-login_totp { background: rgba(14,165,233,0.15); color: #0ea5e9; }

public/js/app.js

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,7 +1662,7 @@ async function renderDashboard() {
16621662
<div style="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">
16631663
16641664
</div>
1665-
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.2.7</div>
1665+
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.2.8</div>
16661666
</div>
16671667
<div class="sidebar-resizer" id="sidebar-resizer"></div>
16681668
</aside>
@@ -1933,6 +1933,10 @@ function renderSidebarNav() {
19331933
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>
19341934
Discord
19351935
</a>
1936+
<a class="nav-item" href="https://ko-fi.com/zerohost" target="_blank">
1937+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-.215 1.472-1.241 1.732-2.463 1.742-.152 0-.303-.001-.454-.003h-4.252c-.229-.023-.406-.222-.406-.449v-7.28c0-.238.189-.428.428-.428h1.923c.237 0 .429.19.429.428v2.079h1.979c2.011 0 3.218.911 3.218 2.743.001 1.155-.434 2.066-1.402 2.168z"/></svg>
1938+
Ko-fi
1939+
</a>
19361940
<a class="nav-item" href="https://www.trustpilot.com/review/zero-host.org" target="_blank">
19371941
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M17.227 16.67l2.19 6.742-7.413-5.388 5.223-1.354zM24 9.31h-9.165L12.005.589l-2.84 8.723L0 9.3l7.422 5.397-2.84 8.714 7.422-5.388 4.583-3.326L24 9.311z"/></svg>
19381942
Leave a Review
@@ -2426,6 +2430,7 @@ let activityIcons = {
24262430
passkey_registered: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M15 7l-3 3-2-2"/><path d="M18.5 8.5a2.121 2.121 0 0 1-3 3"/></svg>',
24272431
email_verified: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="M9 12l2 2 4-4"/></svg>',
24282432
passkey_deleted: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M9.5 7.5L14.5 12.5"/><path d="M14.5 7.5L9.5 12.5"/></svg>',
2433+
passkey_renamed: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M13.5 7.5l1.5 1.5-4 4H9v-2l4.5-4.5z"/></svg>',
24292434
totp_enabled: '<i data-lucide="shield-check" style="width:14px;height:14px;color:#059669"></i>',
24302435
totp_disabled: '<i data-lucide="shield-off" style="width:14px;height:14px;color:#ef4444"></i>',
24312436
login_totp: '<i data-lucide="shield" style="width:14px;height:14px;color:#0ea5e9"></i>',
@@ -2465,6 +2470,7 @@ function getActionLabel(action) {
24652470
passkey_registered: 'Passkey registered',
24662471
email_verified: 'Email verified',
24672472
passkey_deleted: 'Passkey deleted',
2473+
passkey_renamed: 'Passkey renamed',
24682474
totp_enabled: 'Two-factor authentication enabled',
24692475
totp_disabled: 'Two-factor authentication disabled',
24702476
login_totp: 'Signed in with 2FA code',
@@ -3599,18 +3605,29 @@ async function loadPasskeys() {
35993605
}
36003606
list.innerHTML = data.passkeys.map(p => html`
36013607
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:8px">
3602-
<div style="display:flex;align-items:center;gap:10px">
3603-
<i data-lucide="fingerprint" style="width:18px;height:18px;color:var(--accent-1)"></i>
3604-
<div>
3605-
<div style="font-size:0.9rem;font-weight:500">${p.name || 'Passkey'}</div>
3608+
<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0">
3609+
<i data-lucide="fingerprint" style="width:18px;height:18px;color:var(--accent-1);flex-shrink:0"></i>
3610+
<div style="flex:1;min-width:0">
3611+
<div class="passkey-name-row" style="display:flex;align-items:center;gap:6px;cursor:pointer" data-passkey-id="${p.id}">
3612+
<span class="passkey-name-text" style="font-size:0.9rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(p.name || 'Passkey')}</span>
3613+
<i data-lucide="pencil" style="width:12px;height:12px;color:var(--text-muted);flex-shrink:0;opacity:0.5"></i>
3614+
</div>
36063615
<div style="font-size:0.75rem;color:var(--text-muted)">${formatDate(p.created_at)}</div>
36073616
</div>
36083617
</div>
3609-
<button class="btn btn-danger btn-sm" data-passkey-id="${p.id}" style="width:auto;padding:6px 12px;font-size:0.8rem">Delete</button>
3618+
<button class="btn btn-danger btn-sm" data-passkey-id="${p.id}" style="width:auto;padding:6px 12px;font-size:0.8rem;flex-shrink:0;margin-left:8px">Delete</button>
36103619
</div>
36113620
`).join('');
36123621
list.querySelectorAll('[data-passkey-id]').forEach(btn => {
3613-
btn.addEventListener('click', () => handleDeletePasskey(btn.dataset.passkeyId));
3622+
if (btn.tagName === 'BUTTON') {
3623+
btn.addEventListener('click', () => handleDeletePasskey(btn.dataset.passkeyId));
3624+
}
3625+
});
3626+
list.querySelectorAll('.passkey-name-row').forEach(row => {
3627+
row.addEventListener('click', (e) => {
3628+
e.stopPropagation();
3629+
startRenamePasskey(row);
3630+
});
36143631
});
36153632
initIcons();
36163633
} catch (err) {
@@ -3619,6 +3636,52 @@ async function loadPasskeys() {
36193636
}
36203637
}
36213638

3639+
async function startRenamePasskey(row) {
3640+
const id = row.dataset.passkeyId;
3641+
const textEl = row.querySelector('.passkey-name-text');
3642+
const currentName = textEl.textContent;
3643+
3644+
const input = document.createElement('input');
3645+
input.type = 'text';
3646+
input.value = currentName;
3647+
input.maxLength = 255;
3648+
input.style.cssText = 'font-size:0.9rem;font-weight:500;padding:2px 6px;border:1px solid var(--accent-1);border-radius:4px;background:var(--bg-primary);color:var(--text-primary);outline:none;width:100%;min-width:60px';
3649+
3650+
textEl.replaceWith(input);
3651+
input.focus();
3652+
input.select();
3653+
3654+
const finish = async (save) => {
3655+
if (save) {
3656+
const newName = input.value.trim();
3657+
if (newName && newName !== currentName) {
3658+
try {
3659+
await api(`/auth/passkeys/${id}`, {
3660+
method: 'PATCH',
3661+
body: JSON.stringify({ name: newName }),
3662+
});
3663+
showToast('Passkey renamed', 'success');
3664+
loadPasskeys();
3665+
return;
3666+
} catch (err) {
3667+
showToast(err.message, 'error');
3668+
}
3669+
}
3670+
}
3671+
const span = document.createElement('span');
3672+
span.className = 'passkey-name-text';
3673+
span.style.cssText = 'font-size:0.9rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
3674+
span.textContent = currentName;
3675+
input.replaceWith(span);
3676+
};
3677+
3678+
input.addEventListener('blur', () => finish(true));
3679+
input.addEventListener('keydown', (e) => {
3680+
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
3681+
if (e.key === 'Escape') { e.preventDefault(); finish(false); }
3682+
});
3683+
}
3684+
36223685
async function handleRegisterPasskey() {
36233686
const btn = $('#register-passkey-btn');
36243687
const status = $('#passkey-status');

routes/passkeys.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,38 @@ router.get('/passkeys', authenticateToken, passkeyRegisterLimiter, async (req, r
344344
}
345345
});
346346

347+
router.patch('/passkeys/:id', authenticateToken, passkeyRegisterLimiter, async (req, res) => {
348+
try {
349+
const id = parseInt(req.params.id, 10);
350+
if (isNaN(id)) {
351+
return res.status(400).json({ error: 'Invalid passkey ID' });
352+
}
353+
354+
const { name } = req.body;
355+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
356+
return res.status(400).json({ error: 'Name is required' });
357+
}
358+
const trimmed = name.trim().slice(0, 255);
359+
360+
const passkeys = await query(
361+
'SELECT id FROM passkeys WHERE id = ? AND user_id = ?',
362+
[id, req.user.userId]
363+
);
364+
365+
if (!passkeys.length) {
366+
return res.status(404).json({ error: 'Passkey not found' });
367+
}
368+
369+
await query('UPDATE passkeys SET name = ? WHERE id = ?', [trimmed, id]);
370+
await logActivity(req.user.userId, 'passkey_renamed', 'Renamed a passkey');
371+
372+
res.json({ success: true, name: trimmed });
373+
} catch (err) {
374+
console.error('Passkey rename error:', err.message);
375+
res.status(500).json({ error: 'Failed to rename passkey' });
376+
}
377+
});
378+
347379
router.delete('/passkeys/:id', authenticateToken, passkeyRegisterLimiter, async (req, res) => {
348380
try {
349381
const id = parseInt(req.params.id, 10);

server.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ app.use((req, res, next) => {
7373
next();
7474
});
7575

76+
app.use(express.json({ limit: '1mb' }));
77+
app.use(express.urlencoded({ extended: false, limit: '1mb' }));
78+
7679
app.use('/api', advancedBotProtection());
7780
app.use('/api', browserIntegrityCheck());
7881
app.use('/api', vpnProxyProtection());
@@ -138,9 +141,6 @@ app.use(cors({
138141
allowedHeaders: ['Content-Type', 'Authorization'],
139142
}));
140143

141-
app.use(express.json({ limit: '1mb' }));
142-
app.use(express.urlencoded({ extended: false, limit: '1mb' }));
143-
144144
app.use((req, res, next) => {
145145
if (req.path.startsWith('/api/') && !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
146146
return res.status(405).json({ error: 'Method not allowed', requestId: req.requestId });

services/fileLogger.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const actionLabels = [
2929
{ pattern: /^POST \/api\/auth\/passkeys\/login\/begin$/, label: 'Début de connexion par passkey' },
3030
{ pattern: /^POST \/api\/auth\/passkeys\/login\/complete$/, label: 'Connexion par passkey' },
3131
{ pattern: /^GET \/api\/auth\/passkeys$/, label: 'Liste des passkeys' },
32+
{ pattern: /^PATCH \/api\/auth\/passkeys\/\d+$/, label: 'Renommage de passkey' },
3233
{ pattern: /^DELETE \/api\/auth\/passkeys\/\d+$/, label: 'Suppression de passkey' },
3334

3435
// TOTP

services/security.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isIP } from 'net';
2+
import dns from 'dns';
23
import { randomBytes } from 'crypto';
34
import { resolve } from 'path';
45
import { readFile } from 'fs/promises';
@@ -120,19 +121,11 @@ async function checkDnsbl(ip, dnsbl) {
120121
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return false;
121122
if (octets[0] === 192 && octets[1] === 168) return false;
122123
const reverseHost = `${octets[3]}.${octets[2]}.${octets[1]}.${octets[0]}.${dnsbl}`;
123-
const controller = new AbortController();
124-
const timer = setTimeout(() => controller.abort(), 3000);
125-
try {
126-
const res = await fetch(`http://${reverseHost}`, {
127-
signal: controller.signal,
128-
headers: { 'Accept': 'text/plain' },
129-
});
130-
return res.status >= 127;
131-
} catch {
132-
return false;
133-
} finally {
134-
clearTimeout(timer);
135-
}
124+
const addresses = await Promise.race([
125+
dns.promises.resolve4(reverseHost),
126+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
127+
]);
128+
return addresses.some(addr => addr.startsWith('127.0.0.'));
136129
} catch {
137130
return false;
138131
}

0 commit comments

Comments
 (0)