Fix refresh logout+ add trash (undo deleted)
This commit is contained in:
@@ -19,7 +19,6 @@ $db_path = __DIR__ . '/vault.db';
|
|||||||
$db = new SQLite3($db_path);
|
$db = new SQLite3($db_path);
|
||||||
$db->enableExceptions(true);
|
$db->enableExceptions(true);
|
||||||
|
|
||||||
// Create tables
|
|
||||||
$db->exec("
|
$db->exec("
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -46,14 +45,17 @@ $db->exec("
|
|||||||
iv TEXT NOT NULL,
|
iv TEXT NOT NULL,
|
||||||
encryption_method TEXT DEFAULT 'server',
|
encryption_method TEXT DEFAULT 'server',
|
||||||
folder TEXT DEFAULT 'All',
|
folder TEXT DEFAULT 'All',
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
deleted_at DATETIME,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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 encryption_method TEXT DEFAULT 'server'"); } catch (Exception $e) {}
|
||||||
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {}
|
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {}
|
||||||
|
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted INTEGER DEFAULT 0"); } catch (Exception $e) {}
|
||||||
|
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted_at DATETIME"); } catch (Exception $e) {}
|
||||||
|
|
||||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||||
$path = str_replace('/password-manager/api.php', '', $path);
|
$path = str_replace('/password-manager/api.php', '', $path);
|
||||||
@@ -82,7 +84,6 @@ function authenticate($db) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
// Auth
|
|
||||||
case ($path === '/register' && $method === 'POST'):
|
case ($path === '/register' && $method === 'POST'):
|
||||||
$u = trim($input['username'] ?? '');
|
$u = trim($input['username'] ?? '');
|
||||||
$p = $input['masterPassword'] ?? '';
|
$p = $input['masterPassword'] ?? '';
|
||||||
@@ -100,7 +101,6 @@ try {
|
|||||||
$st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
|
$st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
|
||||||
$st->execute();
|
$st->execute();
|
||||||
$uid = $db->lastInsertRowID();
|
$uid = $db->lastInsertRowID();
|
||||||
// Insert default folders
|
|
||||||
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
||||||
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
||||||
foreach ($defaultFolders as $name) {
|
foreach ($defaultFolders as $name) {
|
||||||
@@ -124,7 +124,6 @@ try {
|
|||||||
}
|
}
|
||||||
$key = genKey($p, $user['salt']);
|
$key = genKey($p, $user['salt']);
|
||||||
$token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
|
$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'];
|
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
||||||
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
||||||
foreach ($defaultFolders as $name) {
|
foreach ($defaultFolders as $name) {
|
||||||
@@ -135,16 +134,13 @@ try {
|
|||||||
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]);
|
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Folders
|
|
||||||
case ($path === '/folders' && $method === 'GET'):
|
case ($path === '/folders' && $method === 'GET'):
|
||||||
$auth = authenticate($db);
|
$auth = authenticate($db);
|
||||||
$st = $db->prepare('SELECT name FROM folders WHERE user_id=:uid ORDER BY name');
|
$st = $db->prepare('SELECT name FROM folders WHERE user_id=:uid ORDER BY name');
|
||||||
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
$res = $st->execute();
|
$res = $st->execute();
|
||||||
$folders = [];
|
$folders = [];
|
||||||
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
|
while ($row = $res->fetchArray(SQLITE3_ASSOC)) { $folders[] = $row['name']; }
|
||||||
$folders[] = $row['name'];
|
|
||||||
}
|
|
||||||
echo json_encode($folders);
|
echo json_encode($folders);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -152,13 +148,12 @@ try {
|
|||||||
$auth = authenticate($db);
|
$auth = authenticate($db);
|
||||||
$name = trim($input['name'] ?? '');
|
$name = trim($input['name'] ?? '');
|
||||||
if (!$name) { http_response_code(400); echo json_encode(['error'=>'Folder name required']); break; }
|
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 = $db->prepare('INSERT INTO folders (user_id, name) VALUES (:uid, :name)');
|
||||||
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
$st->bindValue(':name', $name, SQLITE3_TEXT);
|
$st->bindValue(':name', $name, SQLITE3_TEXT);
|
||||||
try { $st->execute(); }
|
try { $st->execute(); } catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Folder exists']); break; }
|
||||||
catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Folder already exists']); break; }
|
echo json_encode(['message'=>'Created','name'=>$name]);
|
||||||
echo json_encode(['message'=>'Folder created', 'name'=>$name]);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case (preg_match('/^\/folders\/(.+)$/', $path, $m) && $method === 'DELETE'):
|
case (preg_match('/^\/folders\/(.+)$/', $path, $m) && $method === 'DELETE'):
|
||||||
@@ -169,8 +164,7 @@ try {
|
|||||||
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
$st->bindValue(':name', $folderName, SQLITE3_TEXT);
|
$st->bindValue(':name', $folderName, SQLITE3_TEXT);
|
||||||
$st->execute();
|
$st->execute();
|
||||||
if ($db->changes() === 0) { http_response_code(404); echo json_encode(['error'=>'Folder not found']); break; }
|
if ($db->changes() === 0) { http_response_code(404); echo json_encode(['error'=>'Not found']); break; }
|
||||||
// Update entries that had this folder to 'All'
|
|
||||||
$stUp = $db->prepare("UPDATE vault_entries SET folder='All' WHERE user_id=:uid AND folder=:f");
|
$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(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
$stUp->bindValue(':f', $folderName, SQLITE3_TEXT);
|
$stUp->bindValue(':f', $folderName, SQLITE3_TEXT);
|
||||||
@@ -178,17 +172,19 @@ try {
|
|||||||
echo json_encode(['message'=>'Deleted']);
|
echo json_encode(['message'=>'Deleted']);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Entries
|
// Entries - exclude deleted by default
|
||||||
case ($path === '/entries' && $method === 'GET'):
|
case ($path === '/entries' && $method === 'GET'):
|
||||||
$auth = authenticate($db);
|
$auth = authenticate($db);
|
||||||
$q = $_GET['search'] ?? '';
|
$q = $_GET['search'] ?? '';
|
||||||
|
$showDeleted = $_GET['deleted'] ?? '0';
|
||||||
if ($q) {
|
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);
|
$st->bindValue(':q', "%$q%", SQLITE3_TEXT);
|
||||||
} else {
|
} 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(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
|
$st->bindValue(':del', $showDeleted === '1' ? 1 : 0, SQLITE3_INTEGER);
|
||||||
$res = $st->execute();
|
$res = $st->execute();
|
||||||
$entries = [];
|
$entries = [];
|
||||||
while ($r = $res->fetchArray(SQLITE3_ASSOC)) {
|
while ($r = $res->fetchArray(SQLITE3_ASSOC)) {
|
||||||
@@ -200,6 +196,8 @@ try {
|
|||||||
'iv' => $r['iv'],
|
'iv' => $r['iv'],
|
||||||
'encryption_method' => $r['encryption_method'] ?? 'server',
|
'encryption_method' => $r['encryption_method'] ?? 'server',
|
||||||
'folder' => $r['folder'] ?? 'All',
|
'folder' => $r['folder'] ?? 'All',
|
||||||
|
'deleted' => $r['deleted'],
|
||||||
|
'deleted_at' => $r['deleted_at'],
|
||||||
'created_at' => $r['created_at'],
|
'created_at' => $r['created_at'],
|
||||||
'updated_at' => $r['updated_at']
|
'updated_at' => $r['updated_at']
|
||||||
];
|
];
|
||||||
@@ -251,15 +249,40 @@ try {
|
|||||||
echo json_encode(['message'=>'Updated']);
|
echo json_encode(['message'=>'Updated']);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'DELETE'):
|
case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'DELETE'):
|
||||||
$auth = authenticate($db);
|
$auth = authenticate($db);
|
||||||
|
$permanent = $_GET['permanent'] ?? '0';
|
||||||
|
if ($permanent === '1') {
|
||||||
$st = $db->prepare('DELETE FROM vault_entries WHERE id=:id AND user_id=:uid');
|
$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(':id', $m[1], SQLITE3_INTEGER);
|
||||||
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
$st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
|
||||||
$st->execute();
|
$st->execute();
|
||||||
echo json_encode(['message'=>'Deleted']);
|
echo json_encode(['message'=>'Deleted']);
|
||||||
break;
|
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:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['error'=>'Not found']);
|
echo json_encode(['error'=>'Not found']);
|
||||||
|
|||||||
+53
-2
@@ -103,8 +103,20 @@ input:focus, select:focus { border-color: var(--accent); }
|
|||||||
.btn-danger { background: var(--danger); }
|
.btn-danger { background: var(--danger); }
|
||||||
|
|
||||||
.input-group { display: flex; gap: 0.5rem; margin: 0.7rem 0; flex-wrap: wrap; align-items: center; }
|
.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-toggle { display: flex; gap: 0.2rem; background: rgba(0,0,0,0.3); padding: 0.2rem; border-radius: 2rem; }
|
||||||
.view-btn {
|
.view-btn {
|
||||||
background: none; border: none; color: var(--text2);
|
background: none; border: none; color: var(--text2);
|
||||||
@@ -424,3 +436,42 @@ input:focus, select:focus { border-color: var(--accent); }
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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);
|
||||||
|
}
|
||||||
+13
-6
@@ -33,8 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vault">
|
<div class="vault">
|
||||||
<h1>🔐 Vault <span>XAMPP</span><button class="btn btn-outline btn-xs" onclick="toggleTheme()" style="margin-left:auto">🌓</button></h1>
|
<h1>🔐 Vault <span>XAMPP</span>
|
||||||
|
<button class="btn btn-outline btn-xs" id="soundToggle" onclick="toggleSound()" title="Sound" style="margin-left:0.3rem;">🔊</button>
|
||||||
|
<button class="btn btn-outline btn-xs" onclick="toggleTheme()" style="margin-left:auto">🌓</button>
|
||||||
|
</h1>
|
||||||
<div id="authSection" class="auth-section">
|
<div id="authSection" class="auth-section">
|
||||||
<div class="auth-tabs"><button class="auth-tab active" onclick="switchTab('login')">Login</button><button class="auth-tab" onclick="switchTab('register')">Register</button></div>
|
<div class="auth-tabs"><button class="auth-tab active" onclick="switchTab('login')">Login</button><button class="auth-tab" onclick="switchTab('register')">Register</button></div>
|
||||||
<div id="loginForm">
|
<div id="loginForm">
|
||||||
@@ -78,15 +80,18 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="search-box"><input type="text" id="searchInput" placeholder="🔍 Search..." oninput="searchEntries()" autocomplete="off"></div>
|
<div class="search-box">
|
||||||
<div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap">
|
<input type="text" id="searchInput" placeholder="🔍 Search..." oninput="searchEntries()" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-actions">
|
||||||
<div class="view-toggle" id="viewToggle">
|
<div class="view-toggle" id="viewToggle">
|
||||||
<button class="view-btn active" data-view="grid">🟫 Grid</button>
|
<button class="view-btn active" data-view="grid">🟫 Grid</button>
|
||||||
<button class="view-btn" data-view="compact">📝 Compact</button>
|
<button class="view-btn" data-view="compact">📝 Compact</button>
|
||||||
<button class="view-btn" data-view="table">📊 Table</button>
|
<button class="view-btn" data-view="table">📊 Table</button>
|
||||||
<button class="view-btn" data-view="list">📋 List</button>
|
<button class="view-btn" data-view="list">📋 List</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline btn-sm" onclick="openGen()">🎲</button>
|
<button class="btn btn-outline btn-sm" id="trashBtn" onclick="toggleTrash()">🗑️ Trash</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="openGen()">🎲 Generate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,7 +101,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="folders-bar" id="foldersBar"></div>
|
<div class="folders-bar" id="foldersBar"></div>
|
||||||
|
<div id="trashActions" class="hidden" style="margin-bottom:.5rem;text-align:right;">
|
||||||
|
<button class="empty-trash-btn" onclick="emptyTrash()">🗑️ Empty Trash</button>
|
||||||
|
</div>
|
||||||
<div id="entriesContainer" class="grid-view"><div style="text-align:center;color:var(--text2);padding:2rem;grid-column:1/-1">📭 No entries</div></div>
|
<div id="entriesContainer" class="grid-view"><div style="text-align:center;color:var(--text2);padding:2rem;grid-column:1/-1">📭 No entries</div></div>
|
||||||
<div style="margin-top:.6rem;text-align:right"><button class="btn btn-outline btn-sm" onclick="exportPasswords()">📤 Export</button></div>
|
<div style="margin-top:.6rem;text-align:right"><button class="btn btn-outline btn-sm" onclick="exportPasswords()">📤 Export</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,23 +9,116 @@ let lockMin = parseInt(localStorage.getItem('autoLockMinutes') || '5');
|
|||||||
let order = JSON.parse(localStorage.getItem('entryOrder') || '[]');
|
let order = JSON.parse(localStorage.getItem('entryOrder') || '[]');
|
||||||
let selectedFolder = localStorage.getItem('selectedFolder') || 'All';
|
let selectedFolder = localStorage.getItem('selectedFolder') || 'All';
|
||||||
let entries = [];
|
let entries = [];
|
||||||
let folders = ['All']; // Will be populated from server
|
let folders = ['All'];
|
||||||
let genPwdVal = '';
|
let genPwdVal = '';
|
||||||
let cryptoKey = null;
|
let cryptoKey = null;
|
||||||
let idleT, warnT, countT;
|
let idleT, warnT, countT;
|
||||||
let draggedId = null;
|
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 ====================
|
// ==================== 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 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) {
|
function showZigzagToast(elem, msg, type) {
|
||||||
const toast = document.createElement('div');
|
const t = document.createElement('div');
|
||||||
toast.className = 'toast-zigzag ' + (type || 'success');
|
t.className = 'toast-zigzag ' + (type || 'success');
|
||||||
toast.textContent = message;
|
t.textContent = msg;
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(t);
|
||||||
const rect = element.getBoundingClientRect();
|
const r = elem.getBoundingClientRect();
|
||||||
toast.style.left = rect.left + 'px';
|
t.style.left = r.left + 'px';
|
||||||
toast.style.top = rect.top + 'px';
|
t.style.top = r.top + 'px';
|
||||||
setTimeout(() => toast.remove(), 1500);
|
setTimeout(() => t.remove(), 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== THEME ====================
|
// ==================== THEME ====================
|
||||||
@@ -46,16 +139,22 @@ 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 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; }
|
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() {
|
async function loadFolders() {
|
||||||
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } });
|
const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
folders = await r.json();
|
const data = await r.json();
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
folders = data.filter(f => typeof f === 'string');
|
||||||
if (!folders.includes('All')) folders.unshift('All');
|
if (!folders.includes('All')) folders.unshift('All');
|
||||||
} else {
|
} else {
|
||||||
folders = ['All'];
|
folders = ['All'];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
folders = ['All'];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
folders = ['All'];
|
folders = ['All'];
|
||||||
}
|
}
|
||||||
@@ -68,14 +167,10 @@ async function addFolderToServer(name) {
|
|||||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
body: JSON.stringify({ name })
|
body: JSON.stringify({ name })
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) { await loadFolders(); return true; }
|
||||||
await loadFolders();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
toast('❌ ' + (d.error || 'Error'), 'error');
|
toast('❌ ' + (d.error || 'Error'), 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,36 +182,24 @@ async function deleteFolderFromServer(name) {
|
|||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
await loadFolders();
|
await loadFolders();
|
||||||
if (selectedFolder === name) {
|
if (selectedFolder === name) { selectedFolder = 'All'; localStorage.setItem('selectedFolder', 'All'); }
|
||||||
selectedFolder = 'All';
|
|
||||||
localStorage.setItem('selectedFolder', 'All');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
}
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
toast('❌ ' + (d.error || 'Error'), 'error');
|
toast('❌ ' + (d.error || 'Error'), 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFolders() {
|
function renderFolders() {
|
||||||
const bar = document.getElementById('foldersBar');
|
const bar = document.getElementById('foldersBar');
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
|
entries.forEach(e => { if (!e.folder || typeof e.folder !== 'string') e.folder = 'All'; });
|
||||||
// Ensure every entry has a string folder
|
|
||||||
entries.forEach(e => {
|
|
||||||
if (!e.folder || typeof e.folder !== 'string') e.folder = 'All';
|
|
||||||
});
|
|
||||||
|
|
||||||
const counts = {};
|
const counts = {};
|
||||||
entries.forEach(e => {
|
entries.forEach(e => { const f = e.folder; counts[f] = (counts[f] || 0) + 1; });
|
||||||
const f = e.folder;
|
|
||||||
counts[f] = (counts[f] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
folders.forEach(f => {
|
folders.forEach(f => {
|
||||||
|
if (!f) return;
|
||||||
const count = counts[f] || 0;
|
const count = counts[f] || 0;
|
||||||
html += `<span class="folder-chip${selectedFolder === f ? ' active' : ''}" onclick="selectFolder('${esc(f)}')">📁 ${esc(f)}<span class="folder-count">${count}</span>${f !== 'All' ? `<button class="folder-delete-btn" onclick="event.stopPropagation();showDeleteFolderConfirm('${esc(f)}')">✕</button>` : ''}</span>`;
|
html += `<span class="folder-chip${selectedFolder === f ? ' active' : ''}" onclick="selectFolder('${esc(f)}')">📁 ${esc(f)}<span class="folder-count">${count}</span>${f !== 'All' ? `<button class="folder-delete-btn" onclick="event.stopPropagation();showDeleteFolderConfirm('${esc(f)}')">✕</button>` : ''}</span>`;
|
||||||
});
|
});
|
||||||
@@ -129,6 +212,7 @@ function populateAddFolderSelect() {
|
|||||||
if (!select) return;
|
if (!select) return;
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
folders.forEach(f => {
|
folders.forEach(f => {
|
||||||
|
if (!f) return;
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = f;
|
option.value = f;
|
||||||
option.textContent = '📁 ' + f;
|
option.textContent = '📁 ' + f;
|
||||||
@@ -148,27 +232,14 @@ function selectFolder(f) {
|
|||||||
function showAddFolderModal() {
|
function showAddFolderModal() {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'custom-modal-overlay show';
|
overlay.className = 'custom-modal-overlay show';
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `<div class="custom-modal"><h3>📁 New Folder</h3><input type="text" id="newFolderName" placeholder="Folder name"><div class="modal-actions"><button class="btn btn-outline btn-sm" id="cancelAddFolder">Cancel</button><button class="btn btn-sm" id="confirmAddFolder">Create</button></div></div>`;
|
||||||
<div class="custom-modal">
|
|
||||||
<h3>📁 New Folder</h3>
|
|
||||||
<input type="text" id="newFolderName" placeholder="Folder name">
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-outline btn-sm" id="cancelAddFolder">Cancel</button>
|
|
||||||
<button class="btn btn-sm" id="confirmAddFolder">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
document.getElementById('cancelAddFolder').onclick = () => overlay.remove();
|
document.getElementById('cancelAddFolder').onclick = () => overlay.remove();
|
||||||
document.getElementById('confirmAddFolder').onclick = async () => {
|
document.getElementById('confirmAddFolder').onclick = async () => {
|
||||||
const name = document.getElementById('newFolderName').value.trim();
|
const name = document.getElementById('newFolderName').value.trim();
|
||||||
if (!name) { toast('Enter a name', 'error'); return; }
|
if (!name) { toast('Enter a name', 'error'); return; }
|
||||||
const success = await addFolderToServer(name);
|
const ok = await addFolderToServer(name);
|
||||||
if (success) {
|
if (ok) { renderFolders(); populateAddFolderSelect(); overlay.remove(); toast('📁 Folder created!'); }
|
||||||
renderFolders();
|
|
||||||
populateAddFolderSelect();
|
|
||||||
overlay.remove();
|
|
||||||
toast('📁 Folder created!');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||||
}
|
}
|
||||||
@@ -176,30 +247,43 @@ function showAddFolderModal() {
|
|||||||
function showDeleteFolderConfirm(folderName) {
|
function showDeleteFolderConfirm(folderName) {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'custom-modal-overlay show';
|
overlay.className = 'custom-modal-overlay show';
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `<div class="custom-modal"><h3>🗑️ Delete Folder</h3><p style="color:var(--text2);margin-bottom:1rem;">Delete "${folderName}"? Entries move to "All".</p><div class="modal-actions"><button class="btn btn-outline btn-sm" id="cancelDeleteFolder">Cancel</button><button class="btn btn-sm btn-danger" id="confirmDeleteFolder">Delete</button></div></div>`;
|
||||||
<div class="custom-modal">
|
|
||||||
<h3>🗑️ Delete Folder</h3>
|
|
||||||
<p style="color:var(--text2);margin-bottom:1rem;">Delete "${folderName}"? Entries will move to "All".</p>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-outline btn-sm" id="cancelDeleteFolder">Cancel</button>
|
|
||||||
<button class="btn btn-sm btn-danger" id="confirmDeleteFolder">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
|
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
|
||||||
document.getElementById('confirmDeleteFolder').onclick = async () => {
|
document.getElementById('confirmDeleteFolder').onclick = async () => {
|
||||||
const success = await deleteFolderFromServer(folderName);
|
const ok = await deleteFolderFromServer(folderName);
|
||||||
if (success) {
|
if (ok) { renderFolders(); populateAddFolderSelect(); render(); overlay.remove(); toast('📁 Folder deleted'); }
|
||||||
renderFolders();
|
|
||||||
populateAddFolderSelect();
|
|
||||||
render();
|
|
||||||
overlay.remove();
|
|
||||||
toast('📁 Folder deleted');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
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 ====================
|
// ==================== INIT ====================
|
||||||
function init() {
|
function init() {
|
||||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
||||||
@@ -220,8 +304,8 @@ function init() {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
updateSoundButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); document.getElementById('showViewBtnToggle').classList.toggle('active', showView); render(); }
|
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(); }
|
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 r = await fetch(API + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt);
|
token = d.token; curUser = u;
|
||||||
sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', 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();
|
await loadFolders();
|
||||||
toast('✅ Login!'); showVault(); loadEntries();
|
toast('✅ Login!');
|
||||||
|
showVault();
|
||||||
|
loadEntries();
|
||||||
} else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; }
|
} else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; }
|
||||||
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
||||||
finally { document.getElementById('loginBtn').disabled = false; }
|
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 r = await fetch(API + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt);
|
token = d.token; curUser = u;
|
||||||
sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', 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();
|
await loadFolders();
|
||||||
toast('✅ Created!'); showVault(); loadEntries();
|
toast('✅ Created!');
|
||||||
|
showVault();
|
||||||
|
loadEntries();
|
||||||
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
||||||
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
||||||
finally { document.getElementById('registerBtn').disabled = false; }
|
finally { document.getElementById('registerBtn').disabled = false; }
|
||||||
@@ -277,7 +373,7 @@ function doLogout() {
|
|||||||
saveUsername();
|
saveUsername();
|
||||||
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
|
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
|
||||||
document.getElementById('idleWarning').classList.remove('show');
|
document.getElementById('idleWarning').classList.remove('show');
|
||||||
token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All'];
|
token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false;
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
document.getElementById('authSection').classList.remove('hidden');
|
document.getElementById('authSection').classList.remove('hidden');
|
||||||
document.getElementById('vaultSection').classList.add('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) {
|
async function loadEntries(q) {
|
||||||
try {
|
try {
|
||||||
let url = API + '/entries';
|
let url = API + '/entries?deleted=' + (showTrash ? '1' : '0');
|
||||||
if (q) url += '?search=' + encodeURIComponent(q);
|
if (q) url += '&search=' + encodeURIComponent(q);
|
||||||
const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } });
|
const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
const raw = await r.json();
|
const raw = await r.json();
|
||||||
@@ -311,9 +407,9 @@ async function loadEntries(q) {
|
|||||||
for (const e of raw) {
|
for (const e of raw) {
|
||||||
if (e.encryption_method === 'client') {
|
if (e.encryption_method === 'client') {
|
||||||
const pw = await decryptPwd(e.encrypted_password, e.iv);
|
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 {
|
} 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);
|
entries = applyOrder(entries);
|
||||||
@@ -327,44 +423,68 @@ async function loadEntries(q) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredEntries() {
|
function getFilteredEntries() {
|
||||||
|
if (showTrash) return entries;
|
||||||
if (selectedFolder === 'All') return entries;
|
if (selectedFolder === 'All') return entries;
|
||||||
return entries.filter(e => (e.folder || 'All') === selectedFolder);
|
return entries.filter(e => (e.folder || 'All') === selectedFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== RENDER ====================
|
||||||
function render() {
|
function render() {
|
||||||
const c = document.getElementById('entriesContainer');
|
const c = document.getElementById('entriesContainer');
|
||||||
c.className = '';
|
c.className = '';
|
||||||
|
if (showTrash) c.classList.add('trash-view');
|
||||||
c.classList.add(view + '-view');
|
c.classList.add(view + '-view');
|
||||||
const filtered = getFilteredEntries();
|
const filtered = getFilteredEntries();
|
||||||
if (!filtered.length) { c.innerHTML = '<div style="text-align:center;color:var(--text2);padding:2rem;grid-column:1/-1">📭 No entries</div>'; return; }
|
if (!filtered.length) { c.innerHTML = '<div style="text-align:center;color:var(--text2);padding:2rem;grid-column:1/-1">' + (showTrash ? '📭 Trash empty' : '📭 No entries') + '</div>'; return; }
|
||||||
if (view === 'table') {
|
if (view === 'table') {
|
||||||
let h = '<table><thead><tr><th>Site</th>' +
|
let h = '<table><thead><tr><th>Site</th>' + (showMail ? '<th>User</th>' : '') + '<th>Password</th>' + (!showTrash ? '<th>Folder</th>' : '<th>Deleted</th>') + '<th>Actions</th></tr></thead><tbody>';
|
||||||
(showMail ? '<th>User</th>' : '') +
|
|
||||||
'<th>Password</th><th>Folder</th><th>Actions</th></tr></thead><tbody>';
|
|
||||||
filtered.forEach(e => {
|
filtered.forEach(e => {
|
||||||
h += '<tr class="table-row-drag" draggable="true" data-id="' + e.id + '">' +
|
h += '<tr class="table-row-drag" draggable="true" data-id="' + e.id + '">' +
|
||||||
'<td>🌐 ' + esc(e.site) + '</td>' +
|
'<td>🌐 ' + esc(e.site) + '</td>' +
|
||||||
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
|
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
|
||||||
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>' +
|
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>';
|
||||||
'<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
if (!showTrash) {
|
||||||
|
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
||||||
'<td class="actions-cell">' +
|
'<td class="actions-cell">' +
|
||||||
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button> ' : '') +
|
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button> ' : '') +
|
||||||
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
||||||
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
||||||
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
||||||
'</td>' +
|
'</td>';
|
||||||
'</tr>';
|
} else {
|
||||||
|
h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' +
|
||||||
|
'<td class="actions-cell">' +
|
||||||
|
'<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️ Restore</button> ' +
|
||||||
|
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
||||||
|
'</td>';
|
||||||
|
}
|
||||||
|
h += '</tr>';
|
||||||
});
|
});
|
||||||
h += '</tbody></table>';
|
h += '</tbody></table>';
|
||||||
c.innerHTML = h;
|
c.innerHTML = h;
|
||||||
}else { c.innerHTML = filtered.map(e => (view === 'grid' ? gridC(e) : view === 'compact' ? compC(e) : listC(e))).join(''); }
|
} else {
|
||||||
|
c.innerHTML = filtered.map(e => {
|
||||||
|
if (view === 'grid') return gridC(e);
|
||||||
|
if (view === 'compact') return compC(e);
|
||||||
|
return listC(e);
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
attachEvents();
|
attachEvents();
|
||||||
setupDrag();
|
setupDrag();
|
||||||
}
|
}
|
||||||
|
|
||||||
function gridC(e) { return '<div class="entry-card" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div><div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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></div>'; }
|
function gridC(e) {
|
||||||
function listC(e) { return '<div class="entry-row" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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></div></div>'; }
|
if (showTrash) return '<div class="entry-card" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="trash-info">🗑️ ' + timeAgo(e.deleted_at) + '</div></div>';
|
||||||
function compC(e) { return '<div class="entry-compact" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span>🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span>👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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>'; }
|
return '<div class="entry-card" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div><div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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></div>';
|
||||||
|
}
|
||||||
|
function listC(e) {
|
||||||
|
if (showTrash) return '<div class="entry-row" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></div></div>';
|
||||||
|
return '<div class="entry-row" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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></div></div>';
|
||||||
|
}
|
||||||
|
function compC(e) {
|
||||||
|
if (showTrash) return '<div class="entry-compact" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span class="compact-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="compact-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></div>';
|
||||||
|
return '<div class="entry-compact" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span>🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span>👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</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>';
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== EVENTS ====================
|
// ==================== EVENTS ====================
|
||||||
function showConfirm(btn, message, callback) {
|
function showConfirm(btn, message, callback) {
|
||||||
@@ -384,14 +504,12 @@ function showConfirm(btn, message, callback) {
|
|||||||
confirm.querySelector('.confirm-no').onclick = () => confirm.remove();
|
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);
|
setTimeout(() => { document.addEventListener('click', function closeConfirm(e) { if (!confirm.contains(e.target) && e.target !== btn) { confirm.remove(); document.removeEventListener('click', closeConfirm); } }); }, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachEvents() {
|
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('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); });
|
||||||
document.querySelectorAll('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); el.textContent = el.textContent === '••••••••' ? el.dataset.pw : '••••••••'; });
|
document.querySelectorAll('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); 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'); } });
|
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() {
|
function setupDrag() {
|
||||||
const c = document.getElementById('entriesContainer'); if (!c) return;
|
const c = document.getElementById('entriesContainer'); if (!c) return;
|
||||||
c.querySelectorAll('[draggable="true"]').forEach(el => {
|
c.querySelectorAll('[draggable="true"]').forEach(el => {
|
||||||
@@ -411,6 +529,7 @@ function openEdit(id) {
|
|||||||
const folderSelect = document.getElementById('editFolder');
|
const folderSelect = document.getElementById('editFolder');
|
||||||
folderSelect.innerHTML = '';
|
folderSelect.innerHTML = '';
|
||||||
folders.forEach(f => {
|
folders.forEach(f => {
|
||||||
|
if (!f) return;
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = f;
|
option.value = f;
|
||||||
option.textContent = '📁 ' + f;
|
option.textContent = '📁 ' + f;
|
||||||
@@ -426,7 +545,6 @@ function openEdit(id) {
|
|||||||
}
|
}
|
||||||
function closeEdit() { document.getElementById('editModal').classList.remove('show'); }
|
function closeEdit() { document.getElementById('editModal').classList.remove('show'); }
|
||||||
function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; }
|
function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; }
|
||||||
|
|
||||||
async function saveEdit() {
|
async function saveEdit() {
|
||||||
const id = document.getElementById('editId').value;
|
const id = document.getElementById('editId').value;
|
||||||
const site = document.getElementById('editSite').value.trim();
|
const site = document.getElementById('editSite').value.trim();
|
||||||
@@ -437,7 +555,7 @@ async function saveEdit() {
|
|||||||
try {
|
try {
|
||||||
const enc = await encryptPwd(password);
|
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 }) });
|
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'); }
|
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
||||||
} catch (e) { toast('⚠️ Error', 'error'); }
|
} catch (e) { toast('⚠️ Error', 'error'); }
|
||||||
}
|
}
|
||||||
@@ -454,31 +572,49 @@ async function addEntry() {
|
|||||||
const enc = await encryptPwd(pass);
|
const enc = await encryptPwd(pass);
|
||||||
const folder = document.getElementById('addFolderSelect').value;
|
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 }) });
|
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'); }
|
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
||||||
} catch (e) { toast('⚠️ Error', 'error'); }
|
} catch (e) { toast('⚠️ Error', 'error'); }
|
||||||
finally { document.getElementById('addBtn').disabled = false; }
|
finally { document.getElementById('addBtn').disabled = false; }
|
||||||
}
|
}
|
||||||
|
async function delEntry(id) {
|
||||||
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'); } }
|
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 ====================
|
// ==================== UTILS ====================
|
||||||
function searchEntries() { loadEntries(document.getElementById('searchInput').value); }
|
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 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; }
|
function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
|
||||||
|
|
||||||
// ==================== STARTUP ====================
|
// ==================== STARTUP – session persistence ====================
|
||||||
init();
|
init();
|
||||||
applyTheme();
|
applyTheme();
|
||||||
|
|
||||||
if (token && curUser) {
|
if (token && curUser) {
|
||||||
// Re-derive key from session? Can't without password, so we need to re-authenticate.
|
const savedPassword = sessionStorage.getItem('masterPassword');
|
||||||
// For now, just show vault if token exists but warn user they must log in again.
|
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();
|
sessionStorage.clear();
|
||||||
token = null;
|
token = null;
|
||||||
curUser = null;
|
curUser = null;
|
||||||
document.getElementById('authSection').classList.remove('hidden');
|
|
||||||
document.getElementById('vaultSection').classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
|
['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.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(); });
|
document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user