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
+53 -20
View File
@@ -18,6 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0);
$db_path = __DIR__ . '/vault.db'; $db_path = __DIR__ . '/vault.db';
$db = new SQLite3($db_path); $db = new SQLite3($db_path);
$db->enableExceptions(true); $db->enableExceptions(true);
$db->busyTimeout(5000);
$db->exec(" $db->exec("
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -25,7 +26,6 @@ $db->exec("
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
salt TEXT NOT NULL, salt TEXT NOT NULL,
encryption_key TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS folders ( CREATE TABLE IF NOT EXISTS folders (
@@ -50,12 +50,23 @@ $db->exec("
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
"); ");
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN encryption_method TEXT DEFAULT 'server'"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE vault_entries ADD COLUMN encryption_method TEXT DEFAULT 'server'"); } catch (Exception $e) {}
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {}
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted INTEGER DEFAULT 0"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted INTEGER DEFAULT 0"); } catch (Exception $e) {}
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted_at DATETIME"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted_at DATETIME"); } catch (Exception $e) {}
try { $db->exec("ALTER TABLE users DROP COLUMN encryption_key"); } catch (Exception $e) {}
$db->exec("DELETE FROM sessions WHERE expires_at < datetime('now')");
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = str_replace('/password-manager/api.php', '', $path); $path = str_replace('/password-manager/api.php', '', $path);
@@ -63,23 +74,22 @@ $path = str_replace('/api.php', '', $path);
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true) ?? []; $input = json_decode(file_get_contents('php://input'), true) ?? [];
function genKey($pwd, $salt) { return hash_pbkdf2('sha256', $pwd, $salt, 100000, 32, true); }
function authenticate($db) { function authenticate($db) {
$headers = getallheaders(); $headers = getallheaders();
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? ''); $token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; } if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; }
$decoded = base64_decode($token); $tokenHash = hash('sha256', $token);
if (!$decoded) { http_response_code(401); echo json_encode(['error'=>'Bad token']); exit; } $st = $db->prepare('SELECT user_id, expires_at FROM sessions WHERE token_hash=:th');
$parts = explode(':', $decoded); $st->bindValue(':th', $tokenHash, SQLITE3_TEXT);
if (count($parts) !== 3) { http_response_code(401); echo json_encode(['error'=>'Token format']); exit; } $session = $st->execute()->fetchArray(SQLITE3_ASSOC);
$uid = intval($parts[0]); if (!$session) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
$key = base64_decode($parts[2]); if (strtotime($session['expires_at']) < time()) {
$st = $db->prepare('SELECT id, encryption_key FROM users WHERE id=:id'); $del = $db->prepare('DELETE FROM sessions WHERE token_hash=:th');
$st->bindValue(':id', $uid, SQLITE3_INTEGER); $del->bindValue(':th', $tokenHash, SQLITE3_TEXT);
$user = $st->execute()->fetchArray(SQLITE3_ASSOC); $del->execute();
if (!$user || $user['encryption_key'] !== base64_encode($key)) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; } http_response_code(401); echo json_encode(['error'=>'Session expired']); exit;
return ['userId' => $uid]; }
return ['userId' => (int)$session['user_id']];
} }
try { try {
@@ -93,12 +103,10 @@ try {
if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; } if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; }
$salt = bin2hex(random_bytes(32)); $salt = bin2hex(random_bytes(32));
$hash = hash_pbkdf2('sha256', $p, $salt, 100000); $hash = hash_pbkdf2('sha256', $p, $salt, 100000);
$key = genKey($p, $salt); $st = $db->prepare('INSERT INTO users (username, password_hash, salt) VALUES (:u, :h, :s)');
$st = $db->prepare('INSERT INTO users (username, password_hash, salt, encryption_key) VALUES (:u, :h, :s, :k)');
$st->bindValue(':u', $u, SQLITE3_TEXT); $st->bindValue(':u', $u, SQLITE3_TEXT);
$st->bindValue(':h', $hash, SQLITE3_TEXT); $st->bindValue(':h', $hash, SQLITE3_TEXT);
$st->bindValue(':s', $salt, SQLITE3_TEXT); $st->bindValue(':s', $salt, SQLITE3_TEXT);
$st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
$st->execute(); $st->execute();
$uid = $db->lastInsertRowID(); $uid = $db->lastInsertRowID();
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
@@ -108,7 +116,14 @@ try {
$stFolder->bindValue(':name', $name, SQLITE3_TEXT); $stFolder->bindValue(':name', $name, SQLITE3_TEXT);
$stFolder->execute(); $stFolder->execute();
} }
$token = base64_encode($uid . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key)); $token = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $token);
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));
$stS = $db->prepare('INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (:uid, :th, :exp)');
$stS->bindValue(':uid', $uid, SQLITE3_INTEGER);
$stS->bindValue(':th', $tokenHash, SQLITE3_TEXT);
$stS->bindValue(':exp', $expires, SQLITE3_TEXT);
$stS->execute();
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$uid,'salt'=>$salt]); echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$uid,'salt'=>$salt]);
break; break;
@@ -122,8 +137,6 @@ try {
if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) { if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) {
http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break; http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break;
} }
$key = genKey($p, $user['salt']);
$token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)'); $stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
foreach ($defaultFolders as $name) { foreach ($defaultFolders as $name) {
@@ -131,6 +144,14 @@ try {
$stFolder->bindValue(':name', $name, SQLITE3_TEXT); $stFolder->bindValue(':name', $name, SQLITE3_TEXT);
$stFolder->execute(); $stFolder->execute();
} }
$token = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $token);
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));
$stS = $db->prepare('INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (:uid, :th, :exp)');
$stS->bindValue(':uid', $user['id'], SQLITE3_INTEGER);
$stS->bindValue(':th', $tokenHash, SQLITE3_TEXT);
$stS->bindValue(':exp', $expires, SQLITE3_TEXT);
$stS->execute();
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]); echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]);
break; break;
@@ -275,6 +296,18 @@ try {
break; break;
// Empty trash (permanently delete all soft-deleted) // Empty trash (permanently delete all soft-deleted)
case ($path === '/logout' && $method === 'POST'):
$headers = getallheaders();
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
if ($token) {
$tokenHash = hash('sha256', $token);
$del = $db->prepare('DELETE FROM sessions WHERE token_hash=:th');
$del->bindValue(':th', $tokenHash, SQLITE3_TEXT);
$del->execute();
}
echo json_encode(['message'=>'Logged out']);
break;
case ($path === '/entries/trash/empty' && $method === 'DELETE'): case ($path === '/entries/trash/empty' && $method === 'DELETE'):
$auth = authenticate($db); $auth = authenticate($db);
$st = $db->prepare('DELETE FROM vault_entries WHERE user_id=:uid AND deleted=1'); $st = $db->prepare('DELETE FROM vault_entries WHERE user_id=:uid AND deleted=1');
+32 -37
View File
@@ -118,9 +118,11 @@ function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark);
// ==================== CRYPTO ==================== // ==================== 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); } 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 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 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 ==================== // ==================== AUTO-LOCK ====================
function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); } function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); }
@@ -249,15 +251,15 @@ function toggleTrash() {
playSound('click'); playSound('click');
} }
async function restoreEntry(id) { 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) { async function permanentDelete(id) {
if (!confirm('Permanently delete?')) return; 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() { async function emptyTrash() {
if (!confirm('Delete ALL trashed entries?')) return; 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) { function timeAgo(dateStr) {
if (!dateStr) return ''; 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 openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); }
function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); } function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); }
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); } 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() { function useGen() {
if (!genPwdVal) genPwd(); if (!genPwdVal) genPwd();
// Put the generated password into the addmodals password field // Put the generated password into the addmodals password field
@@ -388,9 +390,8 @@ async function login() {
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
token = d.token; curUser = u; token = d.token; curUser = u;
sessionStorage.setItem('masterPassword', p);
sessionStorage.setItem('salt', d.salt);
cryptoKey = await deriveKey(p, d.salt); cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token); sessionStorage.setItem('authToken', token);
sessionStorage.setItem('currentUsername', u); sessionStorage.setItem('currentUsername', u);
await loadFolders(); await loadFolders();
@@ -414,9 +415,8 @@ async function register() {
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
token = d.token; curUser = u; token = d.token; curUser = u;
sessionStorage.setItem('masterPassword', p);
sessionStorage.setItem('salt', d.salt);
cryptoKey = await deriveKey(p, d.salt); cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token); sessionStorage.setItem('authToken', token);
sessionStorage.setItem('currentUsername', u); sessionStorage.setItem('currentUsername', u);
await loadFolders(); await loadFolders();
@@ -429,8 +429,11 @@ async function register() {
finally { document.getElementById('registerBtn').disabled = false; } finally { document.getElementById('registerBtn').disabled = false; }
} }
function doLogout() { async function doLogout() {
saveUsername(); saveUsername();
if (token) {
try { await fetch(API + '/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); } catch (e) {}
}
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
document.getElementById('idleWarning').classList.remove('show'); document.getElementById('idleWarning').classList.remove('show');
token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false; 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 + '">' + h += '<tr class="table-row-drag" draggable="true" data-id="' + e.id + '">' +
'<td>🌐 ' + esc(e.site) + '</td>' + '<td>🌐 ' + esc(e.site) + '</td>' +
(showMail ? '<td>👤 ' + esc(e.username) + '</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) { if (!showTrash) {
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' + h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
'<td class="actions-cell">' + '<td class="actions-cell">' +
@@ -554,7 +557,7 @@ function gridC(e) {
if (showMail) html += '<div class="card-user">👤 ' + esc(e.username) + '</div>'; if (showMail) html += '<div class="card-user">👤 ' + esc(e.username) + '</div>';
html += '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div>'; html += '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div>';
if (!showTrash) { 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>' : '') + (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>'; '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div>';
} else { } else {
@@ -581,7 +584,7 @@ function listC(e) {
if (showMail) html += '<span class="entry-user">👤 ' + esc(e.username) + '</span>'; if (showMail) html += '<span class="entry-user">👤 ' + esc(e.username) + '</span>';
if (!showTrash) { if (!showTrash) {
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>'; 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>' : '') + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') +
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>'; '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>';
} else { } else {
@@ -608,7 +611,7 @@ function compC(e) {
if (showMail) html += '<span>👤 ' + esc(e.username) + '</span>'; if (showMail) html += '<span>👤 ' + esc(e.username) + '</span>';
if (!showTrash) { if (!showTrash) {
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>'; 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>'; 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>'; html += '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button>';
} else { } else {
@@ -684,11 +687,12 @@ function showConfirm(btn, message, callback) {
}); });
}, 10); }, 10);
} }
function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; }
function attachEvents() { 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('.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('.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('.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 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('.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() { function setupDrag() {
const c = document.getElementById('entriesContainer'); if (!c) return; const c = document.getElementById('entriesContainer'); if (!c) return;
@@ -799,7 +803,6 @@ async function batchDelete() {
for (const id of selectedIds) await delEntry(id); for (const id of selectedIds) await delEntry(id);
selectedIds.clear(); selectedIds.clear();
hideBatchBar(); hideBatchBar();
loadEntries();
} }
async function batchPermanentDelete() { async function batchPermanentDelete() {
@@ -807,14 +810,12 @@ async function batchPermanentDelete() {
for (const id of selectedIds) await permanentDelete(id); for (const id of selectedIds) await permanentDelete(id);
selectedIds.clear(); selectedIds.clear();
hideBatchBar(); hideBatchBar();
loadEntries();
} }
async function batchRestore() { async function batchRestore() {
for (const id of selectedIds) await restoreEntry(id); for (const id of selectedIds) await restoreEntry(id);
selectedIds.clear(); selectedIds.clear();
hideBatchBar(); hideBatchBar();
loadEntries();
} }
async function batchMove() { async function batchMove() {
@@ -823,10 +824,11 @@ async function batchMove() {
const e = entries.find(x => x.id == id); const e = entries.find(x => x.id == id);
if (e) { if (e) {
e.folder = folder; e.folder = folder;
const enc = await encryptPwd(e.password);
await fetch(API + '/entries/' + id, { await fetch(API + '/entries/' + id, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, 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) { async function delEntry(id) {
try { try {
const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); 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'); } } catch (e) { toast('Error', 'error'); }
} }
@@ -892,25 +894,18 @@ init();
applyTheme(); applyTheme();
if (token && curUser) { if (token && curUser) {
const savedPassword = sessionStorage.getItem('masterPassword'); (async () => {
const savedSalt = sessionStorage.getItem('salt'); if (await restoreCryptoKey()) {
if (savedPassword && savedSalt) { await loadFolders();
deriveKey(savedPassword, savedSalt).then(key => { showVault();
cryptoKey = key; loadEntries();
loadFolders().then(() => { } else {
showVault();
loadEntries();
});
}).catch(() => {
sessionStorage.clear(); sessionStorage.clear();
token = null; token = null;
curUser = null; curUser = null;
}); document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
} else { }
sessionStorage.clear(); })();
token = null;
curUser = null;
}
} }
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); ['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));