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
+
+
+
đī¸ View
@@ -96,7 +101,9 @@
-
+
+
+
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 = `
`;
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 = '
| Site | ' +
- (showMail ? 'User | ' : '') +
- 'Password | Folder | Actions |
';
- filtered.forEach(e => {
- h += '' +
- '| đ ' + esc(e.site) + ' | ' +
- (showMail ? 'đ¤ ' + esc(e.username) + ' | ' : '') +
- 'âĸâĸâĸâĸâĸâĸâĸâĸ | ' +
- 'đ ' + esc(e.folder || 'All') + ' | ' +
- '' +
- (showView ? ' ' : '') +
- ' ' +
- ' ' +
- '' +
- ' | ' +
- '
';
- });
- h += '
';
- c.innerHTML = h;
- }else { c.innerHTML = filtered.map(e => (view === 'grid' ? gridC(e) : view === 'compact' ? compC(e) : listC(e))).join(''); }
+ let h = '
| Site | ' + (showMail ? 'User | ' : '') + 'Password | ' + (!showTrash ? 'Folder | ' : 'Deleted | ') + 'Actions |
';
+ filtered.forEach(e => {
+ h += '' +
+ '| đ ' + esc(e.site) + ' | ' +
+ (showMail ? 'đ¤ ' + esc(e.username) + ' | ' : '') +
+ 'âĸâĸâĸâĸâĸâĸâĸâĸ | ';
+ if (!showTrash) {
+ h += 'đ ' + esc(e.folder || 'All') + ' | ' +
+ '' +
+ (showView ? ' ' : '') +
+ ' ' +
+ ' ' +
+ '' +
+ ' | ';
+ } else {
+ h += 'đī¸ ' + timeAgo(e.deleted_at) + ' | ' +
+ '' +
+ ' ' +
+ '' +
+ ' | ';
+ }
+ h += '
';
+ });
+ h += '
';
+ 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 '
đ ' + esc(e.site) + '' + (showMail ? '
đ¤ ' + esc(e.username) + '' : '') + '
đ ' + esc(e.folder || 'All') + 'âĸâĸâĸâĸâĸâĸâĸâĸ' + (showView ? '' : '') + '
'; }
-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 '
đ ' + 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 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 44a62c3..0000000
Binary files a/password-manager.rar and /dev/null differ
diff --git a/vault.db b/vault.db
index f903189..39c3f96 100644
Binary files a/vault.db and b/vault.db differ