Fix 500 error on batch delete + add SQLite busyTimeout

- Set SQLite busyTimeout(5000) to prevent 'database is locked' on concurrent requests
- Await all loadEntries() in mutation functions to eliminate race conditions
- Remove redundant loadEntries() from batch operations
This commit is contained in:
2026-05-08 22:50:41 +01:00
parent 5ad1498afc
commit 86fc568bda
2 changed files with 85 additions and 57 deletions
+32 -37
View File
@@ -118,9 +118,11 @@ function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark);
// ==================== CRYPTO ====================
function checkStrength() { const p = document.getElementById('passwordInput').value; const b = document.getElementById('strengthBar'); let s = 0; if (p.length >= 8) s++; if (p.length >= 12) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/\d/.test(p)) s++; if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; b.className = 'strength-bar s' + Math.min(4, s); }
async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); }
async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); }
async function encryptPwd(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plain)); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(enc))), iv: btoa(String.fromCharCode(...iv)) }; }
async function decryptPwd(encB64, ivB64) { try { const enc = Uint8Array.from(atob(encB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, enc); return new TextDecoder().decode(dec); } catch (e) { return '[ERROR]'; } }
async function persistCryptoKey() { const raw = await crypto.subtle.exportKey('raw', cryptoKey); sessionStorage.setItem('cryptoKey', btoa(String.fromCharCode(...new Uint8Array(raw)))); }
async function restoreCryptoKey() { const saved = sessionStorage.getItem('cryptoKey'); if (!saved) return false; try { const raw = Uint8Array.from(atob(saved), c => c.charCodeAt(0)); cryptoKey = await crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); return true; } catch (e) { return false; } }
// ==================== AUTO-LOCK ====================
function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); }
@@ -249,15 +251,15 @@ function toggleTrash() {
playSound('click');
}
async function restoreEntry(id) {
try { const r = await fetch(API + '/entries/' + id + '/restore', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('✅ Restored!'); loadEntries(); playSound('success'); } } catch (e) { toast('⚠️ Error', 'error'); }
try { const r = await fetch(API + '/entries/' + id + '/restore', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('✅ Restored!'); await loadEntries(); playSound('success'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
async function permanentDelete(id) {
if (!confirm('Permanently delete?')) return;
try { const r = await fetch(API + '/entries/' + id + '?permanent=1', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('🗑️ Permanently deleted'); loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
try { const r = await fetch(API + '/entries/' + id + '?permanent=1', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('🗑️ Permanently deleted'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
async function emptyTrash() {
if (!confirm('Delete ALL trashed entries?')) return;
try { const r = await fetch(API + '/entries/trash/empty', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('🗑️ Trash emptied'); loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
try { const r = await fetch(API + '/entries/trash/empty', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('🗑️ Trash emptied'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
function timeAgo(dateStr) {
if (!dateStr) return '';
@@ -320,7 +322,7 @@ function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEma
function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); }
function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); }
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; for (let i = 0; i < l; i++) p += c.charAt(Math.floor(Math.random() * c.length)); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; const arr = new Uint32Array(l); crypto.getRandomValues(arr); for (let i = 0; i < l; i++) p += c.charAt(arr[i] % c.length); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
function useGen() {
if (!genPwdVal) genPwd();
// Put the generated password into the addmodals password field
@@ -388,9 +390,8 @@ async function login() {
const d = await r.json();
if (r.ok) {
token = d.token; curUser = u;
sessionStorage.setItem('masterPassword', p);
sessionStorage.setItem('salt', d.salt);
cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('currentUsername', u);
await loadFolders();
@@ -414,9 +415,8 @@ async function register() {
const d = await r.json();
if (r.ok) {
token = d.token; curUser = u;
sessionStorage.setItem('masterPassword', p);
sessionStorage.setItem('salt', d.salt);
cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('currentUsername', u);
await loadFolders();
@@ -429,8 +429,11 @@ async function register() {
finally { document.getElementById('registerBtn').disabled = false; }
}
function doLogout() {
async function doLogout() {
saveUsername();
if (token) {
try { await fetch(API + '/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); } catch (e) {}
}
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
document.getElementById('idleWarning').classList.remove('show');
token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false;
@@ -503,7 +506,7 @@ function render() {
h += '<tr class="table-row-drag" draggable="true" data-id="' + e.id + '">' +
'<td>🌐 ' + esc(e.site) + '</td>' +
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>';
'<td class="password-cell"><span id="p-' + e.id + '">••••••••</span></td>';
if (!showTrash) {
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
'<td class="actions-cell">' +
@@ -554,7 +557,7 @@ function gridC(e) {
if (showMail) html += '<div class="card-user">👤 ' + esc(e.username) + '</div>';
html += '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div>';
if (!showTrash) {
html += '<div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span><div>' +
html += '<div class="card-password"><span id="p-' + e.id + '">••••••••</span><div>' +
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') +
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div>';
} else {
@@ -581,7 +584,7 @@ function listC(e) {
if (showMail) html += '<span class="entry-user">👤 ' + esc(e.username) + '</span>';
if (!showTrash) {
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>';
html += '<div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' +
html += '<div class="password-field"><span class="password-text" id="p-' + e.id + '">••••••••</span>' +
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') +
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>';
} else {
@@ -608,7 +611,7 @@ function compC(e) {
if (showMail) html += '<span>👤 ' + esc(e.username) + '</span>';
if (!showTrash) {
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>';
html += '<span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>';
html += '<span id="p-' + e.id + '">••••••••</span>';
if (showView) html += '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>';
html += '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button>';
} else {
@@ -684,11 +687,12 @@ function showConfirm(btn, message, callback) {
});
}, 10);
}
function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; }
function attachEvents() {
document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); if (showTrash) { permanentDelete(this.dataset.id); } else { showConfirm(this, 'Delete this entry?', id => delEntry(id)); } });
document.querySelectorAll('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); });
document.querySelectorAll('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); el.textContent = el.textContent === '••••••••' ? el.dataset.pw : '••••••••'; });
document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); try { await navigator.clipboard.writeText(el.dataset.pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); playSound('copy'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } });
document.querySelectorAll('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); const pw = entryPw(this.dataset.id); el.textContent = el.textContent === '••••••••' ? pw : '••••••••'; });
document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const pw = entryPw(this.dataset.id); try { await navigator.clipboard.writeText(pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); playSound('copy'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } });
}
function setupDrag() {
const c = document.getElementById('entriesContainer'); if (!c) return;
@@ -799,7 +803,6 @@ async function batchDelete() {
for (const id of selectedIds) await delEntry(id);
selectedIds.clear();
hideBatchBar();
loadEntries();
}
async function batchPermanentDelete() {
@@ -807,14 +810,12 @@ async function batchPermanentDelete() {
for (const id of selectedIds) await permanentDelete(id);
selectedIds.clear();
hideBatchBar();
loadEntries();
}
async function batchRestore() {
for (const id of selectedIds) await restoreEntry(id);
selectedIds.clear();
hideBatchBar();
loadEntries();
}
async function batchMove() {
@@ -823,10 +824,11 @@ async function batchMove() {
const e = entries.find(x => x.id == id);
if (e) {
e.folder = folder;
const enc = await encryptPwd(e.password);
await fetch(API + '/entries/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ site: e.site, username: e.username, encrypted_password: await encryptPwd(e.password), iv: 'batch', folder })
body: JSON.stringify({ site: e.site, username: e.username, encrypted_password: enc.encrypted, iv: enc.iv, folder })
});
}
}
@@ -866,7 +868,7 @@ async function addEntry() {
async function delEntry(id) {
try {
const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); loadEntries(); playSound('delete'); }
if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); }
} catch (e) { toast('Error', 'error'); }
}
@@ -892,25 +894,18 @@ init();
applyTheme();
if (token && curUser) {
const savedPassword = sessionStorage.getItem('masterPassword');
const savedSalt = sessionStorage.getItem('salt');
if (savedPassword && savedSalt) {
deriveKey(savedPassword, savedSalt).then(key => {
cryptoKey = key;
loadFolders().then(() => {
showVault();
loadEntries();
});
}).catch(() => {
(async () => {
if (await restoreCryptoKey()) {
await loadFolders();
showVault();
loadEntries();
} else {
sessionStorage.clear();
token = null;
curUser = null;
});
} else {
sessionStorage.clear();
token = null;
curUser = null;
}
document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
}
})();
}
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));