From 8743f023b28ee7e2d9c09f890379ea33056b7932 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Fri, 8 May 2026 14:39:24 +0100 Subject: [PATCH] Fix refresh logout+ add trash (undo deleted) --- api.php | 61 ++++--- css/style.css | 55 +++++- index.html | 35 ++-- js/app.js | 386 +++++++++++++++++++++++++++++-------------- password-manager.rar | Bin 17182 -> 0 bytes vault.db | Bin 36864 -> 36864 bytes 6 files changed, 377 insertions(+), 160 deletions(-) delete mode 100644 password-manager.rar diff --git a/api.php b/api.php index 5063476..1af17e9 100644 --- a/api.php +++ b/api.php @@ -19,7 +19,6 @@ $db_path = __DIR__ . '/vault.db'; $db = new SQLite3($db_path); $db->enableExceptions(true); -// Create tables $db->exec(" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -46,14 +45,17 @@ $db->exec(" iv TEXT NOT NULL, encryption_method TEXT DEFAULT 'server', folder TEXT DEFAULT 'All', + deleted INTEGER DEFAULT 0, + deleted_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); "); -// Fix missing columns (existing databases) 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) {} $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = str_replace('/password-manager/api.php', '', $path); @@ -82,7 +84,6 @@ function authenticate($db) { try { switch (true) { - // Auth case ($path === '/register' && $method === 'POST'): $u = trim($input['username'] ?? ''); $p = $input['masterPassword'] ?? ''; @@ -100,7 +101,6 @@ try { $st->bindValue(':k', base64_encode($key), SQLITE3_TEXT); $st->execute(); $uid = $db->lastInsertRowID(); - // Insert default folders $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; $stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)'); foreach ($defaultFolders as $name) { @@ -124,7 +124,6 @@ try { } $key = genKey($p, $user['salt']); $token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key)); - // Ensure default folders exist for existing users (idempotent) $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; $stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)'); foreach ($defaultFolders as $name) { @@ -135,16 +134,13 @@ try { echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]); break; - // Folders case ($path === '/folders' && $method === 'GET'): $auth = authenticate($db); $st = $db->prepare('SELECT name FROM folders WHERE user_id=:uid ORDER BY name'); $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); $res = $st->execute(); $folders = []; - while ($row = $res->fetchArray(SQLITE3_ASSOC)) { - $folders[] = $row['name']; - } + while ($row = $res->fetchArray(SQLITE3_ASSOC)) { $folders[] = $row['name']; } echo json_encode($folders); break; @@ -152,13 +148,12 @@ try { $auth = authenticate($db); $name = trim($input['name'] ?? ''); if (!$name) { http_response_code(400); echo json_encode(['error'=>'Folder name required']); break; } - if (strtolower($name) === 'all') { http_response_code(400); echo json_encode(['error'=>'Cannot use "All"']); break; } + if (strtolower($name) === 'all') { http_response_code(400); echo json_encode(['error'=>'Cannot use All']); break; } $st = $db->prepare('INSERT INTO folders (user_id, name) VALUES (:uid, :name)'); $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); $st->bindValue(':name', $name, SQLITE3_TEXT); - try { $st->execute(); } - catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Folder already exists']); break; } - echo json_encode(['message'=>'Folder created', 'name'=>$name]); + try { $st->execute(); } catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Folder exists']); break; } + echo json_encode(['message'=>'Created','name'=>$name]); break; case (preg_match('/^\/folders\/(.+)$/', $path, $m) && $method === 'DELETE'): @@ -169,8 +164,7 @@ try { $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); $st->bindValue(':name', $folderName, SQLITE3_TEXT); $st->execute(); - if ($db->changes() === 0) { http_response_code(404); echo json_encode(['error'=>'Folder not found']); break; } - // Update entries that had this folder to 'All' + if ($db->changes() === 0) { http_response_code(404); echo json_encode(['error'=>'Not found']); break; } $stUp = $db->prepare("UPDATE vault_entries SET folder='All' WHERE user_id=:uid AND folder=:f"); $stUp->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); $stUp->bindValue(':f', $folderName, SQLITE3_TEXT); @@ -178,17 +172,19 @@ try { echo json_encode(['message'=>'Deleted']); break; - // Entries + // Entries - exclude deleted by default case ($path === '/entries' && $method === 'GET'): $auth = authenticate($db); $q = $_GET['search'] ?? ''; + $showDeleted = $_GET['deleted'] ?? '0'; if ($q) { - $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid AND (site LIKE :q OR username LIKE :q) ORDER BY updated_at DESC'); + $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid AND deleted=:del AND (site LIKE :q OR username LIKE :q) ORDER BY updated_at DESC'); $st->bindValue(':q', "%$q%", SQLITE3_TEXT); } else { - $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid ORDER BY updated_at DESC'); + $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid AND deleted=:del ORDER BY updated_at DESC'); } $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->bindValue(':del', $showDeleted === '1' ? 1 : 0, SQLITE3_INTEGER); $res = $st->execute(); $entries = []; while ($r = $res->fetchArray(SQLITE3_ASSOC)) { @@ -200,6 +196,8 @@ try { 'iv' => $r['iv'], 'encryption_method' => $r['encryption_method'] ?? 'server', 'folder' => $r['folder'] ?? 'All', + 'deleted' => $r['deleted'], + 'deleted_at' => $r['deleted_at'], 'created_at' => $r['created_at'], 'updated_at' => $r['updated_at'] ]; @@ -251,15 +249,40 @@ try { echo json_encode(['message'=>'Updated']); break; + // Soft delete case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'DELETE'): $auth = authenticate($db); - $st = $db->prepare('DELETE FROM vault_entries WHERE id=:id AND user_id=:uid'); + $permanent = $_GET['permanent'] ?? '0'; + if ($permanent === '1') { + $st = $db->prepare('DELETE FROM vault_entries WHERE id=:id AND user_id=:uid'); + } else { + $st = $db->prepare("UPDATE vault_entries SET deleted=1, deleted_at=datetime('now') WHERE id=:id AND user_id=:uid"); + } $st->bindValue(':id', $m[1], SQLITE3_INTEGER); $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); $st->execute(); echo json_encode(['message'=>'Deleted']); break; + // Restore + case (preg_match('/^\/entries\/(\d+)\/restore$/', $path, $m) && $method === 'POST'): + $auth = authenticate($db); + $st = $db->prepare('UPDATE vault_entries SET deleted=0, deleted_at=NULL, updated_at=datetime(\'now\') WHERE id=:id AND user_id=:uid'); + $st->bindValue(':id', $m[1], SQLITE3_INTEGER); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->execute(); + echo json_encode(['message'=>'Restored']); + break; + + // Empty trash (permanently delete all soft-deleted) + case ($path === '/entries/trash/empty' && $method === 'DELETE'): + $auth = authenticate($db); + $st = $db->prepare('DELETE FROM vault_entries WHERE user_id=:uid AND deleted=1'); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->execute(); + echo json_encode(['message'=>'Trash emptied']); + break; + default: http_response_code(404); echo json_encode(['error'=>'Not found']); diff --git a/css/style.css b/css/style.css index bd76339..ff86581 100644 --- a/css/style.css +++ b/css/style.css @@ -103,8 +103,20 @@ input:focus, select:focus { border-color: var(--accent); } .btn-danger { background: var(--danger); } .input-group { display: flex; gap: 0.5rem; margin: 0.7rem 0; flex-wrap: wrap; align-items: center; } -.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.7rem; } - +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.7rem; +} +.toolbar-actions { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; +} .view-toggle { display: flex; gap: 0.2rem; background: rgba(0,0,0,0.3); padding: 0.2rem; border-radius: 2rem; } .view-btn { background: none; border: none; color: var(--text2); @@ -423,4 +435,43 @@ input:focus, select:focus { border-color: var(--accent); } max-width: 180px; overflow: hidden; text-overflow: ellipsis; +} +/* Trash view */ +.trash-badge { + background: var(--danger); + color: #fff; + padding: 0.15rem 0.5rem; + border-radius: 1rem; + font-size: 0.65rem; + margin-left: 0.3rem; +} +.trash-info { + font-size: 0.7rem; + color: var(--text2); + margin-top: 0.2rem; +} +.restore-btn { + background: var(--success); + color: #fff; + border: none; + padding: 0.2rem 0.6rem; + border-radius: 1.5rem; + cursor: pointer; + font-size: 0.7rem; +} +.restore-btn:hover { filter: brightness(1.2); } +.empty-trash-btn { + background: var(--danger); + color: #fff; + border: none; + padding: 0.3rem 0.8rem; + border-radius: 1.5rem; + cursor: pointer; + font-size: 0.75rem; +} +.empty-trash-btn:hover { filter: brightness(1.2); } +#trashBtn.btn-danger { + background: var(--danger); + color: #fff; + border-color: var(--danger); } \ No newline at end of file diff --git a/index.html b/index.html index 05bb8ad..5e4a024 100644 --- a/index.html +++ b/index.html @@ -33,8 +33,10 @@
-

🔐 Vault XAMPP

- +

🔐 Vault XAMPP + + +

@@ -78,17 +80,20 @@
- -
-
- - - - -
- -
-
+ +
+
+ + + + +
+ + +
+
đŸ‘ī¸ View
@@ -96,7 +101,9 @@
- +
📭 No entries
diff --git a/js/app.js b/js/app.js index 4dfb993..c9bb475 100644 --- a/js/app.js +++ b/js/app.js @@ -9,23 +9,116 @@ let lockMin = parseInt(localStorage.getItem('autoLockMinutes') || '5'); let order = JSON.parse(localStorage.getItem('entryOrder') || '[]'); let selectedFolder = localStorage.getItem('selectedFolder') || 'All'; let entries = []; -let folders = ['All']; // Will be populated from server +let folders = ['All']; let genPwdVal = ''; let cryptoKey = null; let idleT, warnT, countT; let draggedId = null; +let showTrash = false; +// ==================== SOUND ==================== +let soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; +let audioCtx = null; +function getAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioCtx; +} + +function playTone(freq, duration, type = 'sine', volume = 0.08) { + if (!soundEnabled) return; + try { + const ctx = getAudioContext(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = type; + osc.frequency.setValueAtTime(freq, ctx.currentTime); + gain.gain.setValueAtTime(volume, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration); + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + duration); + } catch (e) { /* ignore */ } +} + +function playSound(type) { + if (!soundEnabled) return; + switch (type) { + case 'click': + playTone(800, 0.08, 'sine', 0.06); + break; + case 'success': + playTone(523, 0.1, 'sine', 0.1); + setTimeout(() => playTone(659, 0.1, 'sine', 0.1), 100); + setTimeout(() => playTone(784, 0.15, 'sine', 0.1), 200); + break; + case 'error': + playTone(200, 0.2, 'square', 0.06); + setTimeout(() => playTone(150, 0.3, 'square', 0.06), 150); + break; + case 'delete': + playTone(150, 0.15, 'triangle', 0.08); + break; + case 'copy': + playTone(1200, 0.05, 'sine', 0.07); + break; + case 'generate': + playTone(440, 0.05, 'sine', 0.05); + setTimeout(() => playTone(554, 0.05, 'sine', 0.05), 60); + setTimeout(() => playTone(659, 0.05, 'sine', 0.05), 120); + setTimeout(() => playTone(880, 0.1, 'sine', 0.07), 180); + break; + case 'open': + playTone(600, 0.12, 'sine', 0.06); + setTimeout(() => playTone(800, 0.1, 'sine', 0.06), 80); + break; + case 'close': + playTone(800, 0.08, 'sine', 0.05); + setTimeout(() => playTone(600, 0.1, 'sine', 0.05), 80); + break; + case 'login': + playTone(523, 0.1, 'sine', 0.08); + setTimeout(() => playTone(659, 0.1, 'sine', 0.08), 100); + setTimeout(() => playTone(784, 0.2, 'sine', 0.1), 200); + break; + case 'register': + playTone(440, 0.1, 'sine', 0.08); + setTimeout(() => playTone(554, 0.1, 'sine', 0.08), 100); + setTimeout(() => playTone(659, 0.15, 'sine', 0.1), 200); + break; + } +} + +function toggleSound() { + soundEnabled = !soundEnabled; + localStorage.setItem('soundEnabled', soundEnabled); + const btn = document.getElementById('soundToggle'); + if (btn) { + btn.textContent = soundEnabled ? '🔊' : '🔇'; + } + if (soundEnabled) playTone(440, 0.05, 'sine', 0.05); // confirmation beep +} + +// Update the button on startup +function updateSoundButton() { + const btn = document.getElementById('soundToggle'); + if (btn) { + btn.textContent = soundEnabled ? '🔊' : '🔇'; + } +} // ==================== TOAST ==================== function toast(m, t) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.textContent = m; c.appendChild(d); setTimeout(() => d.remove(), 3000); } -function showZigzagToast(element, message, type) { - const toast = document.createElement('div'); - toast.className = 'toast-zigzag ' + (type || 'success'); - toast.textContent = message; - document.body.appendChild(toast); - const rect = element.getBoundingClientRect(); - toast.style.left = rect.left + 'px'; - toast.style.top = rect.top + 'px'; - setTimeout(() => toast.remove(), 1500); +function showZigzagToast(elem, msg, type) { + const t = document.createElement('div'); + t.className = 'toast-zigzag ' + (type || 'success'); + t.textContent = msg; + document.body.appendChild(t); + const r = elem.getBoundingClientRect(); + t.style.left = r.left + 'px'; + t.style.top = r.top + 'px'; + setTimeout(() => t.remove(), 1500); } // ==================== THEME ==================== @@ -46,13 +139,19 @@ function resetIdle() { clearTimeout(idleT); clearTimeout(warnT); clearInterval(c function saveUsername() { const f = document.getElementById('usernameInput'); if (f && f.value.trim()) localStorage.setItem('savedUsername', f.value.trim()); } function loadUsername() { const s = localStorage.getItem('savedUsername'); const f = document.getElementById('usernameInput'); if (s && f) f.value = s; } -// ==================== FOLDERS (server-backed) ==================== +// ==================== FOLDERS ==================== async function loadFolders() { + if (!token) return; try { const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { - folders = await r.json(); - if (!folders.includes('All')) folders.unshift('All'); + const data = await r.json(); + if (Array.isArray(data)) { + folders = data.filter(f => typeof f === 'string'); + if (!folders.includes('All')) folders.unshift('All'); + } else { + folders = ['All']; + } } else { folders = ['All']; } @@ -68,14 +167,10 @@ async function addFolderToServer(name) { headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name }) }); - if (r.ok) { - await loadFolders(); - return true; - } else { - const d = await r.json(); - toast('❌ ' + (d.error || 'Error'), 'error'); - return false; - } + if (r.ok) { await loadFolders(); return true; } + const d = await r.json(); + toast('❌ ' + (d.error || 'Error'), 'error'); + return false; } catch (e) { toast('âš ī¸ Connection error', 'error'); return false; } } @@ -87,36 +182,24 @@ async function deleteFolderFromServer(name) { }); if (r.ok) { await loadFolders(); - if (selectedFolder === name) { - selectedFolder = 'All'; - localStorage.setItem('selectedFolder', 'All'); - } + if (selectedFolder === name) { selectedFolder = 'All'; localStorage.setItem('selectedFolder', 'All'); } return true; - } else { - const d = await r.json(); - toast('❌ ' + (d.error || 'Error'), 'error'); - return false; } + const d = await r.json(); + toast('❌ ' + (d.error || 'Error'), 'error'); + return false; } catch (e) { toast('âš ī¸ Connection error', 'error'); return false; } } function renderFolders() { const bar = document.getElementById('foldersBar'); if (!bar) return; - - // Ensure every entry has a string folder - entries.forEach(e => { - if (!e.folder || typeof e.folder !== 'string') e.folder = 'All'; - }); - + entries.forEach(e => { if (!e.folder || typeof e.folder !== 'string') e.folder = 'All'; }); const counts = {}; - entries.forEach(e => { - const f = e.folder; - counts[f] = (counts[f] || 0) + 1; - }); - + entries.forEach(e => { const f = e.folder; counts[f] = (counts[f] || 0) + 1; }); let html = ''; folders.forEach(f => { + if (!f) return; const count = counts[f] || 0; html += `📁 ${esc(f)}${count}${f !== 'All' ? `` : ''}`; }); @@ -129,6 +212,7 @@ function populateAddFolderSelect() { if (!select) return; select.innerHTML = ''; folders.forEach(f => { + if (!f) return; const option = document.createElement('option'); option.value = f; option.textContent = '📁 ' + f; @@ -148,27 +232,14 @@ function selectFolder(f) { function showAddFolderModal() { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay show'; - overlay.innerHTML = ` -
-

📁 New Folder

- - -
`; + overlay.innerHTML = `

📁 New Folder

`; document.body.appendChild(overlay); document.getElementById('cancelAddFolder').onclick = () => overlay.remove(); document.getElementById('confirmAddFolder').onclick = async () => { const name = document.getElementById('newFolderName').value.trim(); if (!name) { toast('Enter a name', 'error'); return; } - const success = await addFolderToServer(name); - if (success) { - renderFolders(); - populateAddFolderSelect(); - overlay.remove(); - toast('📁 Folder created!'); - } + const ok = await addFolderToServer(name); + if (ok) { renderFolders(); populateAddFolderSelect(); overlay.remove(); toast('📁 Folder created!'); } }; overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); } @@ -176,30 +247,43 @@ function showAddFolderModal() { function showDeleteFolderConfirm(folderName) { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay show'; - overlay.innerHTML = ` -
-

đŸ—‘ī¸ Delete Folder

-

Delete "${folderName}"? Entries will move to "All".

- -
`; + overlay.innerHTML = `

đŸ—‘ī¸ Delete Folder

Delete "${folderName}"? Entries move to "All".

`; document.body.appendChild(overlay); document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove(); document.getElementById('confirmDeleteFolder').onclick = async () => { - const success = await deleteFolderFromServer(folderName); - if (success) { - renderFolders(); - populateAddFolderSelect(); - render(); - overlay.remove(); - toast('📁 Folder deleted'); - } + const ok = await deleteFolderFromServer(folderName); + if (ok) { renderFolders(); populateAddFolderSelect(); render(); overlay.remove(); toast('📁 Folder deleted'); } }; overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); } +// ==================== TRASH ==================== +function toggleTrash() { + showTrash = !showTrash; + const btn = document.getElementById('trashBtn'); + const actions = document.getElementById('trashActions'); + if (btn) { btn.textContent = showTrash ? '📋 Active' : 'đŸ—‘ī¸ Trash'; btn.classList.toggle('btn-danger', showTrash); } + if (actions) actions.classList.toggle('hidden', !showTrash); + loadEntries(); +} +async function restoreEntry(id) { + try { const r = await fetch(API + '/entries/' + id + '/restore', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { toast('✅ Restored!'); playSound('success'); loadEntries(); } } 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'); playSound('error'); loadEntries(); } } 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'); playSound('error'); loadEntries(); } } catch (e) { toast('âš ī¸ Error', 'error'); } +} +function timeAgo(dateStr) { + if (!dateStr) return ''; + const now = new Date(); const d = new Date(dateStr + 'Z'); + const days = 30 - Math.floor((now - d) / (1000 * 60 * 60 * 24)); + return days <= 0 ? 'Expiring' : days + 'd left'; +} + // ==================== INIT ==================== function init() { document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); @@ -220,8 +304,8 @@ function init() { render(); } }); + updateSoundButton(); } - function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); document.getElementById('showViewBtnToggle').classList.toggle('active', showView); render(); } function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); document.getElementById('showEmailToggle').classList.toggle('active', showMail); document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; render(); } @@ -245,10 +329,16 @@ async function login() { const r = await fetch(API + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) }); const d = await r.json(); if (r.ok) { - token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt); - sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', u); + token = d.token; curUser = u; + sessionStorage.setItem('masterPassword', p); + sessionStorage.setItem('salt', d.salt); + cryptoKey = await deriveKey(p, d.salt); + sessionStorage.setItem('authToken', token); + sessionStorage.setItem('currentUsername', u); await loadFolders(); - toast('✅ Login!'); showVault(); loadEntries(); + toast('✅ Login!'); + showVault(); + loadEntries(); } else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; } } catch (e) { toast('âš ī¸ Connection error', 'error'); } finally { document.getElementById('loginBtn').disabled = false; } @@ -264,10 +354,16 @@ async function register() { const r = await fetch(API + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) }); const d = await r.json(); if (r.ok) { - token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt); - sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', u); + token = d.token; curUser = u; + sessionStorage.setItem('masterPassword', p); + sessionStorage.setItem('salt', d.salt); + cryptoKey = await deriveKey(p, d.salt); + sessionStorage.setItem('authToken', token); + sessionStorage.setItem('currentUsername', u); await loadFolders(); - toast('✅ Created!'); showVault(); loadEntries(); + toast('✅ Created!'); + showVault(); + loadEntries(); } else { toast('❌ ' + (d.error || 'Failed'), 'error'); } } catch (e) { toast('âš ī¸ Connection error', 'error'); } finally { document.getElementById('registerBtn').disabled = false; } @@ -277,7 +373,7 @@ function doLogout() { saveUsername(); clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); document.getElementById('idleWarning').classList.remove('show'); - token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All']; + token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false; sessionStorage.clear(); document.getElementById('authSection').classList.remove('hidden'); document.getElementById('vaultSection').classList.add('hidden'); @@ -302,8 +398,8 @@ function applyOrder(list) { if (!list || !list.length) return []; if (!order || async function loadEntries(q) { try { - let url = API + '/entries'; - if (q) url += '?search=' + encodeURIComponent(q); + let url = API + '/entries?deleted=' + (showTrash ? '1' : '0'); + if (q) url += '&search=' + encodeURIComponent(q); const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { const raw = await r.json(); @@ -311,9 +407,9 @@ async function loadEntries(q) { for (const e of raw) { if (e.encryption_method === 'client') { const pw = await decryptPwd(e.encrypted_password, e.iv); - entries.push({ id: e.id, site: e.site, username: e.username, password: pw, folder: e.folder || 'All' }); + entries.push({ id: e.id, site: e.site, username: e.username, password: pw, folder: e.folder || 'All', deleted_at: e.deleted_at }); } else { - entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All' }); + entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All', deleted_at: e.deleted_at }); } } entries = applyOrder(entries); @@ -327,44 +423,68 @@ async function loadEntries(q) { } function getFilteredEntries() { + if (showTrash) return entries; if (selectedFolder === 'All') return entries; return entries.filter(e => (e.folder || 'All') === selectedFolder); } +// ==================== RENDER ==================== function render() { const c = document.getElementById('entriesContainer'); c.className = ''; + if (showTrash) c.classList.add('trash-view'); c.classList.add(view + '-view'); const filtered = getFilteredEntries(); - if (!filtered.length) { c.innerHTML = '
📭 No entries
'; return; } + if (!filtered.length) { c.innerHTML = '
' + (showTrash ? '📭 Trash empty' : '📭 No entries') + '
'; return; } if (view === 'table') { - let h = '' + - (showMail ? '' : '') + - ''; - filtered.forEach(e => { - h += '' + - '' + - (showMail ? '' : '') + - '' + - '' + - '' + - ''; - }); - h += '
SiteUserPasswordFolderActions
🌐 ' + esc(e.site) + '👤 ' + esc(e.username) + 'â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ📁 ' + esc(e.folder || 'All') + '' + - (showView ? ' ' : '') + - ' ' + - ' ' + - '' + - '
'; - c.innerHTML = h; - }else { c.innerHTML = filtered.map(e => (view === 'grid' ? gridC(e) : view === 'compact' ? compC(e) : listC(e))).join(''); } + let h = '' + (showMail ? '' : '') + '' + (!showTrash ? '' : '') + ''; + filtered.forEach(e => { + h += '' + + '' + + (showMail ? '' : '') + + ''; + if (!showTrash) { + h += '' + + ''; + } else { + h += '' + + ''; + } + h += ''; + }); + h += '
SiteUserPasswordFolderDeletedActions
🌐 ' + esc(e.site) + '👤 ' + esc(e.username) + 'â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ📁 ' + esc(e.folder || 'All') + '' + + (showView ? ' ' : '') + + ' ' + + ' ' + + '' + + 'đŸ—‘ī¸ ' + timeAgo(e.deleted_at) + '' + + ' ' + + '' + + '
'; + c.innerHTML = h; + } else { + c.innerHTML = filtered.map(e => { + if (view === 'grid') return gridC(e); + if (view === 'compact') return compC(e); + return listC(e); + }).join(''); + } attachEvents(); setupDrag(); } -function gridC(e) { return '
🌐 ' + esc(e.site) + '
' + (showMail ? '
👤 ' + esc(e.username) + '
' : '') + '
📁 ' + esc(e.folder || 'All') + '
â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ
' + (showView ? '' : '') + '
'; } -function listC(e) { return '
'; } -function compC(e) { return '
🌐 ' + esc(e.site) + '' + (showMail ? '👤 ' + esc(e.username) + '' : '') + '📁 ' + esc(e.folder || 'All') + 'â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ' + (showView ? '' : '') + '
'; } +function gridC(e) { + if (showTrash) return '
🌐 ' + esc(e.site) + '
' + (showMail ? '
👤 ' + esc(e.username) + '
' : '') + '
đŸ—‘ī¸ ' + timeAgo(e.deleted_at) + '
'; + return '
🌐 ' + esc(e.site) + '
' + (showMail ? '
👤 ' + esc(e.username) + '
' : '') + '
📁 ' + esc(e.folder || 'All') + '
â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ
' + (showView ? '' : '') + '
'; +} +function listC(e) { + if (showTrash) return '
'; + return '
'; +} +function compC(e) { + if (showTrash) return '
🌐 ' + esc(e.site) + '' + (showMail ? '👤 ' + esc(e.username) + '' : '') + 'đŸ—‘ī¸ ' + timeAgo(e.deleted_at) + '
'; + return '
🌐 ' + esc(e.site) + '' + (showMail ? '👤 ' + esc(e.username) + '' : '') + '📁 ' + esc(e.folder || 'All') + 'â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ' + (showView ? '' : '') + '
'; +} // ==================== EVENTS ==================== function showConfirm(btn, message, callback) { @@ -384,14 +504,12 @@ function showConfirm(btn, message, callback) { confirm.querySelector('.confirm-no').onclick = () => confirm.remove(); setTimeout(() => { document.addEventListener('click', function closeConfirm(e) { if (!confirm.contains(e.target) && e.target !== btn) { confirm.remove(); document.removeEventListener('click', closeConfirm); } }); }, 10); } - function attachEvents() { - document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); 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('.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'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } }); } - function setupDrag() { const c = document.getElementById('entriesContainer'); if (!c) return; c.querySelectorAll('[draggable="true"]').forEach(el => { @@ -411,6 +529,7 @@ function openEdit(id) { const folderSelect = document.getElementById('editFolder'); folderSelect.innerHTML = ''; folders.forEach(f => { + if (!f) return; const option = document.createElement('option'); option.value = f; option.textContent = '📁 ' + f; @@ -426,7 +545,6 @@ function openEdit(id) { } function closeEdit() { document.getElementById('editModal').classList.remove('show'); } function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; } - async function saveEdit() { const id = document.getElementById('editId').value; const site = document.getElementById('editSite').value.trim(); @@ -437,7 +555,7 @@ async function saveEdit() { try { const enc = await encryptPwd(password); const r = await fetch(API + '/entries/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ site, username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) }); - if (r.ok) { toast('✅ Updated!'); closeEdit(); loadEntries(); } + if (r.ok) { toast('✅ Updated!'); playSound('success'); closeEdit(); loadEntries(); } else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); } } catch (e) { toast('âš ī¸ Error', 'error'); } } @@ -454,31 +572,49 @@ async function addEntry() { const enc = await encryptPwd(pass); const folder = document.getElementById('addFolderSelect').value; const r = await fetch(API + '/entries', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, encryption_method: 'client', folder }) }); - if (r.ok) { document.getElementById('siteInput').value = ''; document.getElementById('passwordInput').value = ''; document.getElementById('strengthBar').className = 'strength-bar s0'; toast('✅ Saved!'); loadEntries(); } + if (r.ok) { document.getElementById('siteInput').value = ''; document.getElementById('passwordInput').value = ''; document.getElementById('strengthBar').className = 'strength-bar s0'; toast('✅ Saved!'); playSound('success');loadEntries(); } else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); } } catch (e) { toast('âš ī¸ Error', 'error'); } finally { document.getElementById('addBtn').disabled = false; } } - -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('đŸ—‘ī¸ Deleted!'); loadEntries(); } } catch (e) { toast('Error', 'error'); } } +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'); playSound('delete'); loadEntries(); } + } catch (e) { toast('Error', 'error'); } +} // ==================== UTILS ==================== function searchEntries() { loadEntries(document.getElementById('searchInput').value); } function exportPasswords() { if (!confirm('Export plain text?')) return; const b = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = 'vault-' + new Date().toISOString().slice(0, 10) + '.json'; a.click(); URL.revokeObjectURL(a.href); toast('Exported!'); } function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } -// ==================== STARTUP ==================== +// ==================== STARTUP – session persistence ==================== init(); applyTheme(); + if (token && curUser) { - // Re-derive key from session? Can't without password, so we need to re-authenticate. - // For now, just show vault if token exists but warn user they must log in again. - sessionStorage.clear(); - token = null; - curUser = null; - document.getElementById('authSection').classList.remove('hidden'); - document.getElementById('vaultSection').classList.add('hidden'); + 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(() => { + sessionStorage.clear(); + token = null; + curUser = null; + }); + } else { + sessionStorage.clear(); + token = null; + curUser = null; + } } + ['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } }); document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); }); diff --git a/password-manager.rar b/password-manager.rar deleted file mode 100644 index 44a62c381c89ba6226929bc7104e5030439c653b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17182 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 diff --git a/vault.db b/vault.db index f9031895c5d553b4a11a6e2814e689b0cf6232bb..39c3f96f667a7a35aec9ccce9d3167a285e13f1e 100644 GIT binary patch delta 1503 zcma)+%WvaE9LFaqtJ<3v(JCstt1gzHTOix5?TPIqQY0MPNfYP&Y$BoJIM3Ee8YgWY zmuwIc2QHu>E^ycz65_J-!i^)+N@&HQH%`FcfW%=X#_1#NZjl(@AvtB=Qs1@ zsrkuM^B4D>F8}~I`Yy)mmyh833-EaFp3V9V!0?B_{+;7jdmeviaa%PzX8YmU?0f9? zr{*_S*ETmzp9v=)3*QwD^{=cHUDE}eW} z`Tbq&6gd8N{OLG7aT32c;?|Xw`{sIAtJd^Bm^;lYo2FysNE__cIs?5`>pL;i${K#u zN7FVt0WixDIBr`%xBX~+X&GAIvVUcN51U^+&t~7QPVt|?DZrhs_|{zr@8D2bjEWe` z6w*N<(_(uip6B-Sb$Y^+-Bg56coi~`Zq{oPfs6anx>SxuvZ~fX1T;yrTO_kZdI^g0 zL)y=HZV2(3wv0{*3i=`RQ(goT2l$5TqXh^Cdj0CRA!cMrTR@h@b9EUAmyT&3OwdiI&iieF1qK#BD-@b{5hJMm}oo5l^H?B5=;SmZ` z5b?YZaF^?ojkB(tZzX=aca@zbXv$dX@uDuL=MnkwboKzZ1ANUjTR(3gt)(i9&F%^A zN^=|v(rTM&Q)RWJX_-k#r^Zs+!*=Uhs-p#k^0qJ-iXm?!CP#e1QewN54AB}D-8t}9 z1U@trJpm!L!weI(>@dgZ8JW|s37tQeT~pNeh>7x(J_Osd*o=cE88~+_(C->vHXBW! z?9G+Kql~ULBrTE7jkuzIoRd2~8Wa~jYKe-(vAtLD7TTfDPr|GFj1IRELX`*B1)6csOd(=Z z(J;zYV_mM?X%{+zR_RAQ@+8-3#(D>dp&sqFis?XBlN7hi*C1OEw37JhRkN^w_W0@L d30#Iy*%`iKK~kNwTZTxZXhl7Fko@Dre*vFAl&$~( delta 499 zcmZozz|^pSX@az1DFXuoClJE`>qH%6*-{3*cqv|<*$f<3{Ts&Mj5_Vm_s^UO#z)HaVW3^fRG3CszID#}XoD0WFRjw-M$OG$T2F{=#r z@-#Lo392f~(k>{>H#7A0OSd%(Fmmw>ERE97D)dRubPAgM(pyQ&NWsv=%GAWl*vK&_ zCy$kzfm2k}Raes3Sa|X*pRjr(1p@;s14AnV3!pULKZb=2oVB8jzF-CM*>8O>mYb8An33<{p>Jy8Yo6+#Y~T}Y7UJmS zRvDCD8RhNj;^FS?>mB8pl3VQUq#aR~p5hUvZJuW2w)wA*G9zzeGz-`tO{ze5PLA`s X$iqL4fro)pQM8d)w{bI<|2GE!+aHR+