-
-
-
${p.name || 'Passkey'}
+
+
+
+
+ ${escapeHtml(p.name || 'Passkey')}
+
+
${formatDate(p.created_at)}
-
+
`).join('');
list.querySelectorAll('[data-passkey-id]').forEach(btn => {
- btn.addEventListener('click', () => handleDeletePasskey(btn.dataset.passkeyId));
+ if (btn.tagName === 'BUTTON') {
+ btn.addEventListener('click', () => handleDeletePasskey(btn.dataset.passkeyId));
+ }
+ });
+ list.querySelectorAll('.passkey-name-row').forEach(row => {
+ row.addEventListener('click', (e) => {
+ e.stopPropagation();
+ startRenamePasskey(row);
+ });
});
initIcons();
} catch (err) {
@@ -3619,6 +3636,52 @@ async function loadPasskeys() {
}
}
+async function startRenamePasskey(row) {
+ const id = row.dataset.passkeyId;
+ const textEl = row.querySelector('.passkey-name-text');
+ const currentName = textEl.textContent;
+
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.value = currentName;
+ input.maxLength = 255;
+ 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';
+
+ textEl.replaceWith(input);
+ input.focus();
+ input.select();
+
+ const finish = async (save) => {
+ if (save) {
+ const newName = input.value.trim();
+ if (newName && newName !== currentName) {
+ try {
+ await api(`/auth/passkeys/${id}`, {
+ method: 'PATCH',
+ body: JSON.stringify({ name: newName }),
+ });
+ showToast('Passkey renamed', 'success');
+ loadPasskeys();
+ return;
+ } catch (err) {
+ showToast(err.message, 'error');
+ }
+ }
+ }
+ const span = document.createElement('span');
+ span.className = 'passkey-name-text';
+ span.style.cssText = 'font-size:0.9rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
+ span.textContent = currentName;
+ input.replaceWith(span);
+ };
+
+ input.addEventListener('blur', () => finish(true));
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
+ if (e.key === 'Escape') { e.preventDefault(); finish(false); }
+ });
+}
+
async function handleRegisterPasskey() {
const btn = $('#register-passkey-btn');
const status = $('#passkey-status');
diff --git a/routes/passkeys.js b/routes/passkeys.js
index 36015bc..08ca936 100644
--- a/routes/passkeys.js
+++ b/routes/passkeys.js
@@ -344,6 +344,38 @@ router.get('/passkeys', authenticateToken, passkeyRegisterLimiter, async (req, r
}
});
+router.patch('/passkeys/:id', authenticateToken, passkeyRegisterLimiter, async (req, res) => {
+ try {
+ const id = parseInt(req.params.id, 10);
+ if (isNaN(id)) {
+ return res.status(400).json({ error: 'Invalid passkey ID' });
+ }
+
+ const { name } = req.body;
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
+ return res.status(400).json({ error: 'Name is required' });
+ }
+ const trimmed = name.trim().slice(0, 255);
+
+ const passkeys = await query(
+ 'SELECT id FROM passkeys WHERE id = ? AND user_id = ?',
+ [id, req.user.userId]
+ );
+
+ if (!passkeys.length) {
+ return res.status(404).json({ error: 'Passkey not found' });
+ }
+
+ await query('UPDATE passkeys SET name = ? WHERE id = ?', [trimmed, id]);
+ await logActivity(req.user.userId, 'passkey_renamed', 'Renamed a passkey');
+
+ res.json({ success: true, name: trimmed });
+ } catch (err) {
+ console.error('Passkey rename error:', err.message);
+ res.status(500).json({ error: 'Failed to rename passkey' });
+ }
+});
+
router.delete('/passkeys/:id', authenticateToken, passkeyRegisterLimiter, async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
diff --git a/server.js b/server.js
index d8cf8fa..651ef08 100644
--- a/server.js
+++ b/server.js
@@ -73,6 +73,9 @@ app.use((req, res, next) => {
next();
});
+app.use(express.json({ limit: '1mb' }));
+app.use(express.urlencoded({ extended: false, limit: '1mb' }));
+
app.use('/api', advancedBotProtection());
app.use('/api', browserIntegrityCheck());
app.use('/api', vpnProxyProtection());
@@ -138,9 +141,6 @@ app.use(cors({
allowedHeaders: ['Content-Type', 'Authorization'],
}));
-app.use(express.json({ limit: '1mb' }));
-app.use(express.urlencoded({ extended: false, limit: '1mb' }));
-
app.use((req, res, next) => {
if (req.path.startsWith('/api/') && !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
return res.status(405).json({ error: 'Method not allowed', requestId: req.requestId });
diff --git a/services/fileLogger.js b/services/fileLogger.js
index aeb76a5..5a1e471 100644
--- a/services/fileLogger.js
+++ b/services/fileLogger.js
@@ -29,6 +29,7 @@ const actionLabels = [
{ pattern: /^POST \/api\/auth\/passkeys\/login\/begin$/, label: 'Début de connexion par passkey' },
{ pattern: /^POST \/api\/auth\/passkeys\/login\/complete$/, label: 'Connexion par passkey' },
{ pattern: /^GET \/api\/auth\/passkeys$/, label: 'Liste des passkeys' },
+ { pattern: /^PATCH \/api\/auth\/passkeys\/\d+$/, label: 'Renommage de passkey' },
{ pattern: /^DELETE \/api\/auth\/passkeys\/\d+$/, label: 'Suppression de passkey' },
// TOTP
diff --git a/services/security.js b/services/security.js
index 55d155f..de9432b 100644
--- a/services/security.js
+++ b/services/security.js
@@ -1,4 +1,5 @@
import { isIP } from 'net';
+import dns from 'dns';
import { randomBytes } from 'crypto';
import { resolve } from 'path';
import { readFile } from 'fs/promises';
@@ -120,19 +121,11 @@ async function checkDnsbl(ip, dnsbl) {
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return false;
if (octets[0] === 192 && octets[1] === 168) return false;
const reverseHost = `${octets[3]}.${octets[2]}.${octets[1]}.${octets[0]}.${dnsbl}`;
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 3000);
- try {
- const res = await fetch(`http://${reverseHost}`, {
- signal: controller.signal,
- headers: { 'Accept': 'text/plain' },
- });
- return res.status >= 127;
- } catch {
- return false;
- } finally {
- clearTimeout(timer);
- }
+ const addresses = await Promise.race([
+ dns.promises.resolve4(reverseHost),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
+ ]);
+ return addresses.some(addr => addr.startsWith('127.0.0.'));
} catch {
return false;
}