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:
@@ -18,6 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0);
|
||||
$db_path = __DIR__ . '/vault.db';
|
||||
$db = new SQLite3($db_path);
|
||||
$db->enableExceptions(true);
|
||||
$db->busyTimeout(5000);
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -25,7 +26,6 @@ $db->exec("
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
encryption_key TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
@@ -50,12 +50,23 @@ $db->exec("
|
||||
created_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 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_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 = str_replace('/password-manager/api.php', '', $path);
|
||||
@@ -63,23 +74,22 @@ $path = str_replace('/api.php', '', $path);
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$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) {
|
||||
$headers = getallheaders();
|
||||
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
|
||||
if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; }
|
||||
$decoded = base64_decode($token);
|
||||
if (!$decoded) { http_response_code(401); echo json_encode(['error'=>'Bad token']); exit; }
|
||||
$parts = explode(':', $decoded);
|
||||
if (count($parts) !== 3) { http_response_code(401); echo json_encode(['error'=>'Token format']); exit; }
|
||||
$uid = intval($parts[0]);
|
||||
$key = base64_decode($parts[2]);
|
||||
$st = $db->prepare('SELECT id, encryption_key FROM users WHERE id=:id');
|
||||
$st->bindValue(':id', $uid, SQLITE3_INTEGER);
|
||||
$user = $st->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$user || $user['encryption_key'] !== base64_encode($key)) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
|
||||
return ['userId' => $uid];
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$st = $db->prepare('SELECT user_id, expires_at FROM sessions WHERE token_hash=:th');
|
||||
$st->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$session = $st->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$session) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
|
||||
if (strtotime($session['expires_at']) < time()) {
|
||||
$del = $db->prepare('DELETE FROM sessions WHERE token_hash=:th');
|
||||
$del->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$del->execute();
|
||||
http_response_code(401); echo json_encode(['error'=>'Session expired']); exit;
|
||||
}
|
||||
return ['userId' => (int)$session['user_id']];
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -93,12 +103,10 @@ try {
|
||||
if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; }
|
||||
$salt = bin2hex(random_bytes(32));
|
||||
$hash = hash_pbkdf2('sha256', $p, $salt, 100000);
|
||||
$key = genKey($p, $salt);
|
||||
$st = $db->prepare('INSERT INTO users (username, password_hash, salt, encryption_key) VALUES (:u, :h, :s, :k)');
|
||||
$st = $db->prepare('INSERT INTO users (username, password_hash, salt) VALUES (:u, :h, :s)');
|
||||
$st->bindValue(':u', $u, SQLITE3_TEXT);
|
||||
$st->bindValue(':h', $hash, SQLITE3_TEXT);
|
||||
$st->bindValue(':s', $salt, SQLITE3_TEXT);
|
||||
$st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
|
||||
$st->execute();
|
||||
$uid = $db->lastInsertRowID();
|
||||
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
||||
@@ -108,7 +116,14 @@ try {
|
||||
$stFolder->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$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]);
|
||||
break;
|
||||
|
||||
@@ -122,8 +137,6 @@ try {
|
||||
if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) {
|
||||
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'];
|
||||
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
||||
foreach ($defaultFolders as $name) {
|
||||
@@ -131,6 +144,14 @@ try {
|
||||
$stFolder->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$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']]);
|
||||
break;
|
||||
|
||||
@@ -275,6 +296,18 @@ try {
|
||||
break;
|
||||
|
||||
// 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'):
|
||||
$auth = authenticate($db);
|
||||
$st = $db->prepare('DELETE FROM vault_entries WHERE user_id=:uid AND deleted=1');
|
||||
|
||||
@@ -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 add‑modal’s 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(); }));
|
||||
|
||||
Reference in New Issue
Block a user