From a4528eb1f77521f0f231f95900ac3f7f7d80ea79 Mon Sep 17 00:00:00 2001
From: Zaki <18zaki18@gmail.com>
Date: Wed, 6 May 2026 20:49:51 +0100
Subject: [PATCH] Standard password manager
---
api.php | 273 ++++++++++++++++++++++++
css/style.css | 426 +++++++++++++++++++++++++++++++++++++
index.html | 122 +++++++++++
js/app.js | 485 +++++++++++++++++++++++++++++++++++++++++++
password-manager.rar | Bin 0 -> 17182 bytes
vault.db | Bin 0 -> 36864 bytes
6 files changed, 1306 insertions(+)
create mode 100644 api.php
create mode 100644 css/style.css
create mode 100644 index.html
create mode 100644 js/app.js
create mode 100644 password-manager.rar
create mode 100644 vault.db
diff --git a/api.php b/api.php
new file mode 100644
index 0000000..5063476
--- /dev/null
+++ b/api.php
@@ -0,0 +1,273 @@
+ 'Server error: ' . $e->getMessage()]);
+ exit;
+});
+
+header('Content-Type: application/json');
+header('Access-Control-Allow-Origin: *');
+header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0);
+
+$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,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ salt TEXT NOT NULL,
+ encryption_key TEXT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS folders (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(user_id, name)
+ );
+ CREATE TABLE IF NOT EXISTS vault_entries (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ site TEXT NOT NULL,
+ username TEXT NOT NULL,
+ encrypted_password TEXT NOT NULL,
+ iv TEXT NOT NULL,
+ encryption_method TEXT DEFAULT 'server',
+ folder TEXT DEFAULT 'All',
+ 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) {}
+
+$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
+$path = str_replace('/password-manager/api.php', '', $path);
+$path = str_replace('/api.php', '', $path);
+$method = $_SERVER['REQUEST_METHOD'];
+$input = json_decode(file_get_contents('php://input'), true) ?? [];
+
+function genKey($pwd, $salt) { return hash_pbkdf2('sha256', $pwd, $salt, 100000, 32, true); }
+
+function authenticate($db) {
+ $headers = getallheaders();
+ $token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
+ if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; }
+ $decoded = base64_decode($token);
+ if (!$decoded) { http_response_code(401); echo json_encode(['error'=>'Bad token']); exit; }
+ $parts = explode(':', $decoded);
+ if (count($parts) !== 3) { http_response_code(401); echo json_encode(['error'=>'Token format']); exit; }
+ $uid = intval($parts[0]);
+ $key = base64_decode($parts[2]);
+ $st = $db->prepare('SELECT id, encryption_key FROM users WHERE id=:id');
+ $st->bindValue(':id', $uid, SQLITE3_INTEGER);
+ $user = $st->execute()->fetchArray(SQLITE3_ASSOC);
+ if (!$user || $user['encryption_key'] !== base64_encode($key)) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
+ return ['userId' => $uid];
+}
+
+try {
+ switch (true) {
+ // Auth
+ case ($path === '/register' && $method === 'POST'):
+ $u = trim($input['username'] ?? '');
+ $p = $input['masterPassword'] ?? '';
+ if (strlen($u) < 3 || strlen($p) < 8) { http_response_code(400); echo json_encode(['error'=>'Min 3/8 chars']); break; }
+ $st = $db->prepare('SELECT id FROM users WHERE username=:u');
+ $st->bindValue(':u', $u, SQLITE3_TEXT);
+ if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; }
+ $salt = bin2hex(random_bytes(32));
+ $hash = hash_pbkdf2('sha256', $p, $salt, 100000);
+ $key = genKey($p, $salt);
+ $st = $db->prepare('INSERT INTO users (username, password_hash, salt, encryption_key) VALUES (:u, :h, :s, :k)');
+ $st->bindValue(':u', $u, SQLITE3_TEXT);
+ $st->bindValue(':h', $hash, SQLITE3_TEXT);
+ $st->bindValue(':s', $salt, SQLITE3_TEXT);
+ $st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
+ $st->execute();
+ $uid = $db->lastInsertRowID();
+ // 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) {
+ $stFolder->bindValue(':uid', $uid, SQLITE3_INTEGER);
+ $stFolder->bindValue(':name', $name, SQLITE3_TEXT);
+ $stFolder->execute();
+ }
+ $token = base64_encode($uid . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
+ echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$uid,'salt'=>$salt]);
+ break;
+
+ case ($path === '/login' && $method === 'POST'):
+ $u = trim($input['username'] ?? '');
+ $p = $input['masterPassword'] ?? '';
+ $st = $db->prepare('SELECT * FROM users WHERE username=:u');
+ $st->bindValue(':u', $u, SQLITE3_TEXT);
+ $user = $st->execute()->fetchArray(SQLITE3_ASSOC);
+ if (!$user) { http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break; }
+ if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) {
+ http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break;
+ }
+ $key = genKey($p, $user['salt']);
+ $token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
+ // 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) {
+ $stFolder->bindValue(':uid', $user['id'], SQLITE3_INTEGER);
+ $stFolder->bindValue(':name', $name, SQLITE3_TEXT);
+ $stFolder->execute();
+ }
+ 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'];
+ }
+ echo json_encode($folders);
+ break;
+
+ case ($path === '/folders' && $method === 'POST'):
+ $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; }
+ $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]);
+ break;
+
+ case (preg_match('/^\/folders\/(.+)$/', $path, $m) && $method === 'DELETE'):
+ $auth = authenticate($db);
+ $folderName = urldecode($m[1]);
+ if ($folderName === 'All') { http_response_code(400); echo json_encode(['error'=>'Cannot delete All']); break; }
+ $st = $db->prepare('DELETE FROM folders WHERE user_id=:uid AND name=:name');
+ $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'
+ $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);
+ $stUp->execute();
+ echo json_encode(['message'=>'Deleted']);
+ break;
+
+ // Entries
+ case ($path === '/entries' && $method === 'GET'):
+ $auth = authenticate($db);
+ $q = $_GET['search'] ?? '';
+ 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->bindValue(':q', "%$q%", SQLITE3_TEXT);
+ } else {
+ $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid ORDER BY updated_at DESC');
+ }
+ $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
+ $res = $st->execute();
+ $entries = [];
+ while ($r = $res->fetchArray(SQLITE3_ASSOC)) {
+ $entries[] = [
+ 'id' => $r['id'],
+ 'site' => $r['site'],
+ 'username' => $r['username'],
+ 'encrypted_password' => $r['encrypted_password'],
+ 'iv' => $r['iv'],
+ 'encryption_method' => $r['encryption_method'] ?? 'server',
+ 'folder' => $r['folder'] ?? 'All',
+ 'created_at' => $r['created_at'],
+ 'updated_at' => $r['updated_at']
+ ];
+ }
+ echo json_encode($entries);
+ break;
+
+ case ($path === '/entries' && $method === 'POST'):
+ $auth = authenticate($db);
+ $site = trim($input['site'] ?? '');
+ $username = trim($input['username'] ?? '');
+ $folder = trim($input['folder'] ?? 'All');
+ $encPwd = $input['encrypted_password'] ?? '';
+ $iv = $input['iv'] ?? '';
+ if (empty($site) || empty($encPwd)) { http_response_code(400); echo json_encode(['error'=>'Site & password required']); break; }
+ $now = date('Y-m-d H:i:s');
+ $st = $db->prepare('INSERT INTO vault_entries (user_id, site, username, encrypted_password, iv, encryption_method, folder, created_at, updated_at) VALUES (:uid,:s,:u,:e,:i,:m,:f,:c,:c)');
+ $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
+ $st->bindValue(':s', $site, SQLITE3_TEXT);
+ $st->bindValue(':u', $username, SQLITE3_TEXT);
+ $st->bindValue(':e', $encPwd, SQLITE3_TEXT);
+ $st->bindValue(':i', $iv, SQLITE3_TEXT);
+ $st->bindValue(':m', 'client', SQLITE3_TEXT);
+ $st->bindValue(':f', $folder, SQLITE3_TEXT);
+ $st->bindValue(':c', $now, SQLITE3_TEXT);
+ $st->execute();
+ echo json_encode(['id'=>$db->lastInsertRowID(), 'site'=>$site, 'username'=>$username, 'folder'=>$folder]);
+ break;
+
+ case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'PUT'):
+ $auth = authenticate($db);
+ $site = trim($input['site'] ?? '');
+ $username = trim($input['username'] ?? '');
+ $encPwd = $input['encrypted_password'] ?? '';
+ $iv = $input['iv'] ?? '';
+ $folder = trim($input['folder'] ?? 'All');
+ if (empty($site) || empty($encPwd)) { http_response_code(400); echo json_encode(['error'=>'Site & password required']); break; }
+ $now = date('Y-m-d H:i:s');
+ $st = $db->prepare('UPDATE vault_entries SET site=:s, username=:u, encrypted_password=:e, iv=:i, folder=:f, updated_at=:c WHERE id=:id AND user_id=:uid');
+ $st->bindValue(':s', $site, SQLITE3_TEXT);
+ $st->bindValue(':u', $username, SQLITE3_TEXT);
+ $st->bindValue(':e', $encPwd, SQLITE3_TEXT);
+ $st->bindValue(':i', $iv, SQLITE3_TEXT);
+ $st->bindValue(':f', $folder, SQLITE3_TEXT);
+ $st->bindValue(':c', $now, SQLITE3_TEXT);
+ $st->bindValue(':id', $m[1], SQLITE3_INTEGER);
+ $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
+ $st->execute();
+ echo json_encode(['message'=>'Updated']);
+ break;
+
+ 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');
+ $st->bindValue(':id', $m[1], SQLITE3_INTEGER);
+ $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
+ $st->execute();
+ echo json_encode(['message'=>'Deleted']);
+ break;
+
+ default:
+ http_response_code(404);
+ echo json_encode(['error'=>'Not found']);
+ }
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['error' => $e->getMessage()]);
+}
+
+$db->close();
+?>
\ No newline at end of file
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..bd76339
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,426 @@
+:root {
+ --bg: #0a0e17;
+ --bg2: #131a24;
+ --text: #e1e8f0;
+ --text2: #8899aa;
+ --accent: #3b82f6;
+ --danger: #ef4444;
+ --success: #22c55e;
+ --warning: #f59e0b;
+ --border: rgba(255,255,255,0.06);
+ --card: rgba(18,25,35,0.85);
+ --input: #10161f;
+ --vault-bg: rgba(255,255,255,0.03);
+}
+.light {
+ --bg: #f4f6f9;
+ --bg2: #e9ecf1;
+ --text: #1a1f2e;
+ --text2: #5a6475;
+ --card: rgba(255,255,255,0.9);
+ --input: #ffffff;
+ --border: rgba(0,0,0,0.08);
+ --vault-bg: rgba(255,255,255,0.5);
+}
+
+* { margin:0; padding:0; box-sizing:border-box; }
+body {
+ background: linear-gradient(145deg, var(--bg) 0%, var(--bg2) 100%);
+ font-family: 'Segoe UI', system-ui, sans-serif;
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 1.2rem;
+ color: var(--text);
+ transition: background 0.3s, color 0.3s;
+}
+
+.toast-container {
+ position: fixed; top: 1rem; right: 1rem; z-index: 9999;
+ display: flex; flex-direction: column; gap: 0.5rem;
+}
+.toast {
+ padding: 0.7rem 1.2rem; border-radius: 0.8rem;
+ font-size: 0.85rem; animation: slideIn 0.3s ease; color: #fff;
+}
+.toast.error { background: var(--danger); }
+.toast.success { background: var(--success); }
+@keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+.vault {
+ width: 96%; max-width: 1700px;
+ background: var(--vault-bg);
+ backdrop-filter: blur(20px);
+ border: 1px solid var(--border);
+ border-radius: 2rem; padding: 1.8rem; margin: 0.5rem auto;
+ box-shadow: 0 30px 50px rgba(0,0,0,0.4);
+}
+h1 {
+ font-size: 2rem; margin-bottom: 0.6rem;
+ display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
+}
+h1 span {
+ background: var(--accent); padding: 0.2rem 0.7rem;
+ border-radius: 3rem; font-size: 0.8rem; color: #fff;
+}
+
+/* Auth */
+.auth-section {
+ background: rgba(0,0,0,0.25); border-radius: 1.5rem;
+ padding: 1.3rem; margin-bottom: 1rem; border: 1px solid var(--border);
+}
+.auth-tabs { display: flex; gap: 1rem; margin-bottom: 0.8rem; }
+.auth-tab {
+ background: none; border: none; color: var(--text2);
+ padding: 0.4rem 0.8rem; cursor: pointer;
+ border-bottom: 2px solid transparent; font-size: 0.9rem;
+}
+.auth-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
+
+input, select, textarea {
+ flex: 1; min-width: 130px; background: var(--input);
+ border: 1px solid #2d3748; padding: 0.65rem 1rem;
+ border-radius: 2rem; color: var(--text); font-size: 0.85rem;
+ outline: none; font-family: inherit;
+}
+input:focus, select:focus { border-color: var(--accent); }
+
+.btn {
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
+ border: none; color: #fff; font-weight: 600;
+ padding: 0.65rem 1.3rem; border-radius: 2rem;
+ cursor: pointer; font-size: 0.85rem; transition: 0.2s; white-space: nowrap;
+}
+.btn:hover { filter: brightness(1.15); transform: scale(1.02); }
+.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
+.btn-outline { background: transparent; border: 1px solid #475569; color: var(--text2); }
+.btn-sm { padding: 0.35rem 0.9rem; font-size: 0.75rem; }
+.btn-xs { padding: 0.2rem 0.5rem; font-size: 0.7rem; }
+.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; }
+
+.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);
+ padding: 0.35rem 0.7rem; border-radius: 2rem; cursor: pointer;
+ font-size: 0.75rem; transition: 0.2s;
+}
+.view-btn.active { background: var(--accent); color: #fff; }
+.view-btn:hover:not(.active) { background: rgba(255,255,255,0.08); }
+
+.toggles-row { display: flex; gap: 1.2rem; align-items: center; flex-wrap: wrap; }
+.toggle-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.75rem; color: var(--text2); }
+.toggle-switch {
+ position: relative; width: 36px; height: 20px;
+ background: #334155; border-radius: 10px; cursor: pointer; transition: 0.2s;
+}
+.toggle-switch.active { background: var(--accent); }
+.toggle-switch::after {
+ content: ''; position: absolute; top: 2px; left: 2px;
+ width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: 0.2s;
+}
+.toggle-switch.active::after { left: 18px; }
+
+.status-badge {
+ background: var(--bg2); padding: 0.25rem 0.8rem;
+ border-radius: 2rem; font-size: 0.75rem; white-space: nowrap;
+}
+.hidden { display: none !important; }
+
+/* Strength */
+.strength-bar { height: 4px; border-radius: 2px; transition: 0.3s; margin-top: 0.2rem; }
+.s0 { background: var(--danger); width: 20%; }
+.s1 { background: var(--warning); width: 40%; }
+.s2 { background: #eab308; width: 60%; }
+.s3 { background: #84cc16; width: 80%; }
+.s4 { background: var(--success); width: 100%; }
+
+/* Folders */
+.folders-bar {
+ display: flex; gap: 0.4rem; margin-bottom: 0.8rem;
+ flex-wrap: wrap; align-items: center;
+ padding: 0.4rem 0.6rem; background: rgba(0,0,0,0.2); border-radius: 1rem;
+}
+.folder-chip {
+ background: var(--bg2); border: 1px solid var(--border);
+ color: var(--text2); padding: 0.3rem 0.8rem; border-radius: 2rem;
+ cursor: pointer; font-size: 0.78rem; transition: 0.2s; white-space: nowrap;
+}
+.folder-chip:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
+.folder-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.folder-count {
+ background: rgba(0,0,0,0.3); padding: 0.1rem 0.4rem;
+ border-radius: 1rem; margin-left: 0.3rem; font-size: 0.7rem;
+}
+.folder-delete-btn {
+ background: transparent; border: none; color: var(--danger);
+ cursor: pointer; font-size: 0.7rem; margin-left: 0.2rem; opacity: 0.7;
+}
+.folder-delete-btn:hover { opacity: 1; }
+
+.folder-add-btn {
+ background: transparent; border: 1px dashed #475569;
+ color: var(--text2); padding: 0.3rem 0.6rem; border-radius: 2rem;
+ cursor: pointer; font-size: 0.75rem; transition: 0.2s;
+}
+.folder-add-btn:hover { border-color: var(--accent); color: var(--accent); }
+
+/* Add form folder select */
+#addFolderSelect {
+ min-width: 110px; max-width: 150px;
+ background: var(--input); border: 1px solid #2d3748;
+ color: var(--text); padding: 0.5rem 0.8rem; border-radius: 2rem;
+ font-size: 0.8rem; cursor: pointer;
+}
+
+/* Entries */
+#entriesContainer.grid-view {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 0.6rem;
+}
+#entriesContainer.list-view { display: flex; flex-direction: column; gap: 0.4rem; }
+#entriesContainer.compact-view { display: flex; flex-direction: column; gap: 0.2rem; }
+#entriesContainer.table-view { overflow-x: auto; }
+#entriesContainer.table-view table { width: 100%; border-collapse: collapse; }
+#entriesContainer.table-view th {
+ text-align: left; padding: 0.4rem 0.6rem;
+ color: var(--text2); font-size: 0.75rem; border-bottom: 1px solid var(--border);
+}
+#entriesContainer.table-view td {
+ padding: 0.4rem 0.6rem; font-size: 0.8rem;
+ border-bottom: 1px solid rgba(255,255,255,0.03);
+}
+
+.entry-card, .entry-row, .entry-compact, .table-row-drag {
+ cursor: grab; user-select: none;
+}
+.entry-card.drag-over, .entry-row.drag-over, .entry-compact.drag-over, .table-row-drag.drag-over {
+ border-color: var(--accent) !important;
+ box-shadow: 0 0 15px rgba(59,130,246,0.3);
+}
+
+.entry-card {
+ background: var(--card);
+ border: 1px solid rgba(255, 255, 255, 0.1); /* brighter border */
+ border-radius: 0.9rem;
+ padding: 1rem 2.5rem 1rem 1rem;
+ transition: 0.2s;
+ position: relative;
+ word-break: break-word;
+}
+
+.entry-card:hover { border-color: rgba(255,255,255,0.15); transform: translateY(-2px); }
+.card-site { font-weight: 700; font-size: 0.9rem; color: var(--text); margin-bottom: 0.2rem; }
+.card-user { color: var(--text2); font-size: 0.75rem; margin-bottom: 0.3rem; }
+.card-folder {
+ font-size: 0.65rem; color: var(--accent); margin-bottom: 0.3rem;
+ background: rgba(59,130,246,0.15); display: inline-block;
+ padding: 0.1rem 0.5rem; border-radius: 1rem;
+}
+.card-password {
+ background: var(--bg2); padding: 0.3rem 0.5rem; border-radius: 0.6rem;
+ display: flex; align-items: center; justify-content: space-between;
+ font-family: monospace; font-size: 0.75rem; gap: 0.2rem;
+}
+.entry-row {
+ background: var(--card); border: 1px solid var(--border);
+ border-radius: 0.8rem; padding: 0.7rem 2.8rem 0.7rem 0.9rem;
+ display: flex; justify-content: space-between; align-items: center;
+ flex-wrap: wrap; gap: 0.4rem; position: relative;
+}
+.entry-compact {
+ display: flex; align-items: center; gap: 0.5rem;
+ padding: 0.35rem 2.8rem 0.35rem 0.7rem;
+ background: var(--card); border-radius: 0.5rem;
+ border: 1px solid var(--border); font-size: 0.8rem; position: relative;
+}
+
+.action-btns { position: absolute; top: 4px; right: 6px; display: flex; gap: 4px; z-index: 1; }
+.delete-btn {
+ width: 20px; height: 20px; background: transparent; color: var(--danger);
+ border: none; cursor: pointer; font-size: 0.9rem; font-weight: 700;
+ display: flex; align-items: center; justify-content: center;
+}
+.delete-btn:hover { color: #fff; transform: scale(1.2); }
+.light .delete-btn:hover { color: #000 !important; }
+.edit-btn {
+ width: 20px; height: 20px; background: transparent; color: var(--accent);
+ border: none; cursor: pointer; font-size: 0.75rem;
+ display: flex; align-items: center; justify-content: center;
+}
+.edit-btn:hover { color: #fff; transform: scale(1.2); }
+.light .edit-btn:hover { color: #000 !important; }
+
+.entry-info { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; flex: 1; }
+.entry-site { font-weight: 700; color: var(--text); }
+.entry-user { color: var(--text2); }
+.entry-folder {
+ font-size: 0.7rem; color: var(--accent);
+ background: rgba(59,130,246,0.15); padding: 0.1rem 0.5rem; border-radius: 1rem;
+}
+.password-field {
+ display: flex; align-items: center; gap: 0.3rem;
+ background: var(--bg2); padding: 0.2rem 0.5rem; border-radius: 2rem;
+}
+.password-text { font-family: monospace; color: var(--text2); font-size: 0.8rem; }
+.icon-btn {
+ background: none; border: 1px solid #475569; color: var(--text2);
+ border-radius: 2rem; padding: 0.2rem 0.5rem; font-size: 0.65rem; cursor: pointer;
+}
+.icon-btn:hover { background: #334155; }
+
+.search-box { flex: 1; min-width: 160px; }
+.idle-warning {
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
+ background: rgba(0,0,0,0.95); color: #fff; padding: 2rem;
+ border-radius: 2rem; z-index: 10000; text-align: center; display: none;
+}
+.idle-warning.show { display: block; }
+
+/* Edit Modal */
+.edit-modal {
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.7); display: flex; justify-content: center;
+ align-items: center; z-index: 1001; display: none;
+}
+.edit-modal.show { display: flex; }
+.edit-box {
+ background: var(--bg2); border-radius: 1.5rem; padding: 1.5rem;
+ min-width: 380px; max-width: 90%;
+}
+.edit-box h3 { margin-bottom: 1rem; color: var(--text); }
+.edit-box label { color: var(--text2); font-size: 0.8rem; display: block; margin-bottom: 0.2rem; }
+.edit-box input, .edit-box select { width: 100%; margin-bottom: 0.5rem; }
+
+/* Custom modals (folder add/delete) */
+.custom-modal-overlay {
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.6); display: flex; justify-content: center;
+ align-items: center; z-index: 10001; display: none;
+}
+.custom-modal-overlay.show { display: flex; }
+.custom-modal {
+ background: var(--bg2); border: 1px solid var(--border);
+ border-radius: 1.2rem; padding: 1.5rem; min-width: 300px; max-width: 90%;
+}
+.custom-modal h3 { margin-bottom: 1rem; color: var(--text); }
+.custom-modal input { width: 100%; margin-bottom: 1rem; }
+.custom-modal .modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
+
+/* Confirm popup near element */
+.custom-confirm {
+ position: fixed; background: var(--bg2); border: 1px solid var(--accent);
+ border-radius: 0.8rem; padding: 0.7rem 1rem; z-index: 9999;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: none;
+ font-size: 0.8rem; color: var(--text); white-space: nowrap;
+}
+.custom-confirm.show { display: block; }
+.custom-confirm .confirm-text { margin-bottom: 0.5rem; }
+.custom-confirm .confirm-btns { display: flex; gap: 0.4rem; }
+.custom-confirm .confirm-yes {
+ background: var(--danger); color: #fff; border: none;
+ padding: 0.3rem 0.8rem; border-radius: 1.5rem; cursor: pointer; font-size: 0.75rem;
+}
+.custom-confirm .confirm-no {
+ background: #334155; color: #fff; border: none;
+ padding: 0.3rem 0.8rem; border-radius: 1.5rem; cursor: pointer; font-size: 0.75rem;
+}
+
+/* Zigzag toast */
+/* .toast-zigzag { */
+ /* position: fixed; z-index: 9998; padding: 0.5rem 0.9rem; */
+ /* border-radius: 0.5rem; font-size: 0.78rem; font-weight: 600; */
+ /* pointer-events: none; animation: zigzagUp 1.4s ease-out forwards; white-space: nowrap; */
+/* } */
+/* .toast-zigzag.success { background: rgba(34,197,94,0.95); color: #fff; } */
+/* .toast-zigzag.error { background: rgba(239,68,68,0.95); color: #fff; } */
+/* @keyframes zigzagUp { */
+ /* 0% { opacity: 1; transform: translate(0,0) scale(1); } */
+ /* 15% { opacity: 1; transform: translate(-12px,-12px) scale(1.05); } */
+ /* 30% { opacity: 0.9; transform: translate(14px,-24px) scale(1); } */
+ /* 50% { opacity: 0.7; transform: translate(-10px,-38px) scale(0.95); } */
+ /* 70% { opacity: 0.4; transform: translate(10px,-52px) scale(0.9); } */
+ /* 100% { opacity: 0; transform: translate(0,-68px) scale(0.8); } */
+/* } */
+/* Zigzag toast – smaller over time, more transparent */
+.toast-zigzag {
+ position: fixed;
+ z-index: 9998;
+ padding: 0.4rem 0.7rem;
+ border-radius: 0.5rem;
+ font-size: 0.72rem;
+ font-weight: 600;
+ pointer-events: none;
+ animation: zigzagUp 1.2s ease-out forwards;
+ white-space: nowrap;
+}
+.toast-zigzag.success {
+ background: rgba(34, 197, 94, 0.95);
+ color: #fff;
+}
+.toast-zigzag.error {
+ background: rgba(239, 68, 68, 0.95);
+ color: #fff;
+}
+
+@keyframes zigzagUp {
+ 0% {
+ opacity: 1;
+ transform: translate(0, 0) scale(1);
+ }
+ 20% {
+ opacity: 0.9;
+ transform: translate(-10px, -14px) scale(0.9);
+ }
+ 40% {
+ opacity: 0.65;
+ transform: translate(12px, -28px) scale(0.75);
+ }
+ 70% {
+ opacity: 0.3;
+ transform: translate(-8px, -42px) scale(0.6);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(0, -60px) scale(0.4);
+ }
+}
+@media(max-width:700px) {
+ .vault {
+ background: rgba(20, 30, 45, 0.25); /* a bit more visible */
+ backdrop-filter: blur(24px);
+ }
+ .entry-row { flex-direction: column; }
+ #entriesContainer.grid-view { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
+ .toolbar { flex-direction: column; }
+ .folders-bar { flex-direction: column; align-items: stretch; }
+}
+/* Table view improvements */
+#entriesContainer.table-view {
+ overflow-x: auto;
+}
+
+#entriesContainer.table-view table {
+ min-width: 600px; /* Ensures columns don't squash too much */
+ table-layout: auto;
+}
+
+#entriesContainer.table-view th,
+#entriesContainer.table-view td {
+ white-space: nowrap;
+ padding: 0.5rem 0.8rem;
+}
+
+/* Limit password cell width and show ellipsis */
+#entriesContainer.table-view td.password-cell {
+ max-width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..05bb8ad
--- /dev/null
+++ b/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+ 🔐 Vault
+
+
+
+
+ ⏰ Auto-lock
Vault locks in 30s
+
+
+
+
+
🔐 Vault XAMPP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🟢 Connected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..4dfb993
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,485 @@
+const API = '/password-manager/api.php';
+let token = sessionStorage.getItem('authToken');
+let curUser = sessionStorage.getItem('currentUsername');
+let view = localStorage.getItem('vaultView') || 'grid';
+let showView = localStorage.getItem('showViewBtn') !== 'false';
+let showMail = localStorage.getItem('showEmail') !== 'false';
+let dark = localStorage.getItem('darkTheme') !== 'false';
+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 genPwdVal = '';
+let cryptoKey = null;
+let idleT, warnT, countT;
+let draggedId = null;
+
+// ==================== 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);
+}
+
+// ==================== THEME ====================
+function applyTheme() { document.body.classList.toggle('light', !dark); }
+function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark); applyTheme(); }
+
+// ==================== CRYPTO ====================
+function checkStrength() { const p = document.getElementById('passwordInput').value; const b = document.getElementById('strengthBar'); let s = 0; if (p.length >= 8) s++; if (p.length >= 12) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/\d/.test(p)) s++; if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; b.className = 'strength-bar s' + Math.min(4, s); }
+async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); }
+async function encryptPwd(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plain)); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(enc))), iv: btoa(String.fromCharCode(...iv)) }; }
+async function decryptPwd(encB64, ivB64) { try { const enc = Uint8Array.from(atob(encB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, enc); return new TextDecoder().decode(dec); } catch (e) { return '[ERROR]'; } }
+
+// ==================== AUTO-LOCK ====================
+function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); }
+function resetIdle() { clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); document.getElementById('idleWarning').classList.remove('show'); if (lockMin > 0 && token) { const lm = lockMin * 60000; warnT = setTimeout(() => { document.getElementById('idleWarning').classList.add('show'); let cd = 30; document.getElementById('idleCountdown').textContent = cd; countT = setInterval(() => { cd--; document.getElementById('idleCountdown').textContent = cd; if (cd <= 0) { clearInterval(countT); doLogout(); } }, 1000); }, Math.max(0, lm - 30000)); idleT = setTimeout(() => doLogout(), lm); } }
+
+// ==================== USERNAME ====================
+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) ====================
+async function loadFolders() {
+ 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');
+ } else {
+ folders = ['All'];
+ }
+ } catch (e) {
+ folders = ['All'];
+ }
+}
+
+async function addFolderToServer(name) {
+ try {
+ const r = await fetch(API + '/folders', {
+ method: 'POST',
+ 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;
+ }
+ } catch (e) { toast('⚠️ Connection error', 'error'); return false; }
+}
+
+async function deleteFolderFromServer(name) {
+ try {
+ const r = await fetch(API + '/folders/' + encodeURIComponent(name), {
+ method: 'DELETE',
+ headers: { 'Authorization': 'Bearer ' + token }
+ });
+ if (r.ok) {
+ await loadFolders();
+ if (selectedFolder === name) {
+ selectedFolder = 'All';
+ localStorage.setItem('selectedFolder', 'All');
+ }
+ return true;
+ } else {
+ 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';
+ });
+
+ const counts = {};
+ entries.forEach(e => {
+ const f = e.folder;
+ counts[f] = (counts[f] || 0) + 1;
+ });
+
+ let html = '';
+ folders.forEach(f => {
+ const count = counts[f] || 0;
+ html += `📁 ${esc(f)}${count}${f !== 'All' ? `` : ''}`;
+ });
+ html += ``;
+ bar.innerHTML = html;
+}
+
+function populateAddFolderSelect() {
+ const select = document.getElementById('addFolderSelect');
+ if (!select) return;
+ select.innerHTML = '';
+ folders.forEach(f => {
+ const option = document.createElement('option');
+ option.value = f;
+ option.textContent = '📁 ' + f;
+ if (f === selectedFolder) option.selected = true;
+ select.appendChild(option);
+ });
+}
+
+function selectFolder(f) {
+ selectedFolder = f;
+ localStorage.setItem('selectedFolder', f);
+ renderFolders();
+ populateAddFolderSelect();
+ render();
+}
+
+function showAddFolderModal() {
+ const overlay = document.createElement('div');
+ overlay.className = 'custom-modal-overlay show';
+ overlay.innerHTML = `
+
+
📁 New Folder
+
+
+
+
+
+
`;
+ document.body.appendChild(overlay);
+ document.getElementById('cancelAddFolder').onclick = () => overlay.remove();
+ document.getElementById('confirmAddFolder').onclick = async () => {
+ const name = document.getElementById('newFolderName').value.trim();
+ if (!name) { toast('Enter a name', 'error'); return; }
+ const success = await addFolderToServer(name);
+ if (success) {
+ renderFolders();
+ populateAddFolderSelect();
+ overlay.remove();
+ toast('📁 Folder created!');
+ }
+ };
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
+}
+
+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".
+
+
+
+
+
`;
+ 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');
+ }
+ };
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
+}
+
+// ==================== INIT ====================
+function init() {
+ document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
+ document.getElementById('showViewBtnToggle').classList.toggle('active', showView);
+ document.getElementById('showEmailToggle').classList.toggle('active', showMail);
+ document.getElementById('autoLockTimer').value = lockMin;
+ applyTheme();
+ document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
+ loadUsername();
+ const sl = localStorage.getItem('savedLoginUser');
+ if (sl) document.getElementById('loginUsername').value = sl;
+ document.getElementById('viewToggle').addEventListener('click', e => {
+ if (e.target.classList.contains('view-btn')) {
+ document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
+ e.target.classList.add('active');
+ view = e.target.dataset.view;
+ localStorage.setItem('vaultView', view);
+ 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(); }
+
+// ==================== GENERATOR ====================
+function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); }
+function closeGen() { document.getElementById('genModal').style.display = 'none'; }
+function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
+function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; for (let i = 0; i < l; i++) p += c.charAt(Math.floor(Math.random() * c.length)); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
+function useGen() { if (!genPwdVal) genPwd(); document.getElementById('passwordInput').value = genPwdVal; checkStrength(); navigator.clipboard.writeText(genPwdVal); toast('🎲 Copied!'); closeGen(); }
+
+// ==================== AUTH ====================
+function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); }
+
+async function login() {
+ const u = document.getElementById('loginUsername').value.trim();
+ const p = document.getElementById('loginPassword').value;
+ if (!u || !p) { toast('Fill all fields', 'error'); return; }
+ document.getElementById('loginBtn').disabled = true;
+ localStorage.setItem('savedLoginUser', u);
+ try {
+ 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);
+ await loadFolders();
+ 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; }
+}
+
+async function register() {
+ const u = document.getElementById('regUsername').value.trim();
+ const p = document.getElementById('regPassword').value;
+ if (u.length < 3) { toast('Username min 3', 'error'); return; }
+ if (p.length < 8) { toast('Password min 8', 'error'); return; }
+ document.getElementById('registerBtn').disabled = true;
+ try {
+ 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);
+ await loadFolders();
+ toast('✅ Created!'); showVault(); loadEntries();
+ } else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
+ } catch (e) { toast('⚠️ Connection error', 'error'); }
+ finally { document.getElementById('registerBtn').disabled = false; }
+}
+
+function doLogout() {
+ saveUsername();
+ clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
+ document.getElementById('idleWarning').classList.remove('show');
+ token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All'];
+ sessionStorage.clear();
+ document.getElementById('authSection').classList.remove('hidden');
+ document.getElementById('vaultSection').classList.add('hidden');
+ document.getElementById('loginPassword').value = '';
+ document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
+}
+
+function showVault() {
+ document.getElementById('authSection').classList.add('hidden');
+ document.getElementById('vaultSection').classList.remove('hidden');
+ document.getElementById('currentUser').textContent = '👤 ' + curUser;
+ document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
+ document.getElementById('autoLockTimer').value = lockMin;
+ loadUsername();
+ renderFolders();
+ populateAddFolderSelect();
+ resetIdle();
+}
+
+// ==================== ENTRIES ====================
+function applyOrder(list) { if (!list || !list.length) return []; if (!order || !order.length) return list; const map = new Map(list.filter(e => e && e.id).map(e => [e.id, e])); const ord = []; order.forEach(id => { if (map.has(id)) { ord.push(map.get(id)); map.delete(id); } }); map.forEach(e => ord.push(e)); return ord; }
+
+async function loadEntries(q) {
+ try {
+ let url = API + '/entries';
+ if (q) url += '?search=' + encodeURIComponent(q);
+ const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } });
+ if (r.ok) {
+ const raw = await r.json();
+ entries = [];
+ 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' });
+ } else {
+ entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All' });
+ }
+ }
+ entries = applyOrder(entries);
+ document.getElementById('connectionStatus').textContent = '🟢 Connected';
+ document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)';
+ renderFolders();
+ populateAddFolderSelect();
+ render();
+ } else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); }
+ } catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); }
+}
+
+function getFilteredEntries() {
+ if (selectedFolder === 'All') return entries;
+ return entries.filter(e => (e.folder || 'All') === selectedFolder);
+}
+
+function render() {
+ const c = document.getElementById('entriesContainer');
+ c.className = '';
+ c.classList.add(view + '-view');
+ const filtered = getFilteredEntries();
+ if (!filtered.length) { c.innerHTML = '📭 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(''); }
+ 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 ? '
' : '') + '
'; }
+
+// ==================== EVENTS ====================
+function showConfirm(btn, message, callback) {
+ const id = btn.dataset.id;
+ const existing = document.querySelector('.custom-confirm');
+ if (existing) existing.remove();
+ const confirm = document.createElement('div');
+ confirm.className = 'custom-confirm show';
+ confirm.innerHTML = '' + message + '
';
+ document.body.appendChild(confirm);
+ const rect = btn.getBoundingClientRect();
+ confirm.style.top = (rect.top - 60) + 'px';
+ let leftPos = rect.left - confirm.offsetWidth + rect.width;
+ if (leftPos < 10) leftPos = 10;
+ confirm.style.left = leftPos + 'px';
+ confirm.querySelector('.confirm-yes').onclick = () => { confirm.remove(); callback(id); showZigzagToast(btn, '🗑️ Deleted!', 'error'); };
+ 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('.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 => {
+ el.ondragstart = function(e) { draggedId = this.dataset.id; this.style.opacity = '0.5'; e.dataTransfer.setData('text/plain', this.dataset.id); e.dataTransfer.effectAllowed = 'move'; };
+ el.ondragend = function(e) { this.style.opacity = '1'; draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); };
+ el.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dataset.id !== draggedId) this.classList.add('drag-over'); };
+ el.ondragleave = function(e) { this.classList.remove('drag-over'); };
+ el.ondrop = function(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); const fromId = parseInt(e.dataTransfer.getData('text/plain')); const toId = parseInt(this.dataset.id); if (!fromId || !toId || fromId === toId) return; let fi = -1, ti = -1; for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id === fromId) fi = i; if (entries[i] && entries[i].id === toId) ti = i; } if (fi > -1 && ti > -1 && fi !== ti) { const moved = entries.splice(fi, 1)[0]; entries.splice(ti, 0, moved); order = entries.map(e => e.id); localStorage.setItem('entryOrder', JSON.stringify(order)); render(); } };
+ });
+}
+
+// ==================== EDIT ====================
+function openEdit(id) {
+ let e = null;
+ for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id == id) { e = entries[i]; break; } }
+ if (!e) return;
+ const folderSelect = document.getElementById('editFolder');
+ folderSelect.innerHTML = '';
+ folders.forEach(f => {
+ const option = document.createElement('option');
+ option.value = f;
+ option.textContent = '📁 ' + f;
+ if (f === (e.folder || 'All')) option.selected = true;
+ folderSelect.appendChild(option);
+ });
+ document.getElementById('editId').value = id;
+ document.getElementById('editSite').value = e.site;
+ document.getElementById('editUsername').value = e.username;
+ document.getElementById('editPassword').value = e.password;
+ document.getElementById('editPassword').type = 'password';
+ document.getElementById('editModal').classList.add('show');
+}
+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();
+ const username = document.getElementById('editUsername').value.trim();
+ const password = document.getElementById('editPassword').value;
+ const folder = document.getElementById('editFolder').value;
+ if (!site || !password) { toast('Site and password required', 'error'); return; }
+ 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(); }
+ else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
+ } catch (e) { toast('⚠️ Error', 'error'); }
+}
+
+// ==================== ADD / DELETE ====================
+async function addEntry() {
+ const site = document.getElementById('siteInput').value.trim();
+ const user = document.getElementById('usernameInput').value.trim();
+ const pass = document.getElementById('passwordInput').value;
+ if (!site || !pass) { toast('❌ Site and password required', 'error'); return; }
+ if (user) localStorage.setItem('savedUsername', user);
+ document.getElementById('addBtn').disabled = true;
+ try {
+ 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(); }
+ 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'); } }
+
+// ==================== 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 ====================
+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');
+}
+['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(); });
+document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
\ No newline at end of file
diff --git a/password-manager.rar b/password-manager.rar
new file mode 100644
index 0000000000000000000000000000000000000000..44a62c381c89ba6226929bc7104e5030439c653b
GIT binary patch
literal 17182
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
literal 0
HcmV?d00001
diff --git a/vault.db b/vault.db
new file mode 100644
index 0000000000000000000000000000000000000000..f9031895c5d553b4a11a6e2814e689b0cf6232bb
GIT binary patch
literal 36864
zcmeI)%WvDr9ROf5k{^ogaMQpLQa4?0vVrZ&uKCn!;b!%qthZ$9X>Cz~FG;khCn?!7
zum`6>|Abz8YA?Mkut3kf_0ZEEx7Qwe&AG^>MTfHN$m^)+V~g!BegnxOXULi1H#4*}
zqF6Xi>y}0~dc%%nku3HehU3`BB#B|z2t8fsc^#I}g7bO>z2bArixx-NkL%yxSpOBa
z>MCOoeoK7upyVpAdDe{ws~ev!+6m%800ck)1V8`;K;YXa@N?0*;@;lIf1y~CY-nR?
zWLR~rYYlbHytT3v5rt4uAd8`JS|D#FkvsRvSA?c5wgX-o2^HtHmuL>lFb##Y$=_7yM<}LbHm92^*?5ysG(i5!)fd*xr5llcg2+))xM%
zFm2SU4b9oh@~wuPMg9+)@Sio&f2bA9i9+HiJMCIKvre>2ib704z7e57+HvNNuI`e#
zEaFcKsN+ULg-9qWyfrUn6UQatdjCDruITR6c6Wby*&W)#F}|d=+#Ae(>HnB_1Vokmfao?esU`_Z7@RPUc1h3
zw(5Lmmb1hiyYuXs+FniiVJ%9VJN>n_$)c_Ptx}{eN`>J#m7@
z&;tSpfB*=900@8p2!H?xfB*=900@AcVU~C8!0hRRYD&ova<$8anapgEb7R9WU@BU5i>3NfAOUwFf%i;~w2
zOq!liR7z)PqjSP=Opy`7XXkq6kf9jwKE>}-G)c2TmJjmGeRNzn@diT=2p|9gAOHd&
z00JNY0w4eaAOHd&00Q4JffdK5%bvclzyEg-f5M2r5`RH25I_I~KmY_l00ck)1V8`;
zKmY_l00h1@0&5P(7C!qOjrj1MV;jHq(=yj8?r^)NKLprV|0%Y-CSb(<2jtr8jo+>R
z4Xr=`0T2KI5C8!X009sH0T5U$aQUHY9dm7OUOw4>%(9ORU9o@ZOiF4;?@B0y+6|$a
zEgPYukUo^?tU6A#B--Pv(j}@GJvQ>S;i()qqYbv!4~*4jO!ZC*$pp(-;>GyX(;xJ_
zbSB$8^yZmp;&@a$I31*$dRSBp9olS!sx)%}s}UV};+Z6`f2BN{WSAh&2O0ld
zc@!0-=pf}sE>l-T4KFjrao=x&z`X_`n>otC+3T-eomxo*&luMV&{V~
z8#l@?;=6k<8pGZ3haZK;hv}a^8`D)=un;jPp??A=WY}v8tQG$?3Rb
z=u)c}kMsVFPs=G(y5L23)QC;Q=A@PqqVc0tCRIzQ9Wxd7RL4yVnwQV*Hne%{P>F}eN1o_F
z%N|+nVW%?EqlJiGGZONNmW&R^5xFu9NxF|eFCO_$S+mVZ?RG7ZPZTDp13x4AyAk8K
zB2RK=UhbIT(Zrjuys{Xp1ro#4aMtSh)MP3U3#Wum!H5*cg5vX;d~r|?P@VQ!naNa|
z(Sg5_i-ryl`_-Jlo(+}WiNYntPW#RQ(#OdVWx#tKVl)#4j<_0X{&N^Y33?x*YbA
zKD=DAk5oZHR+42p!cUU@1Bxq(#}O&p_WRm8C+7>IFPm58O16>cSK|4yew?U|PAtKg
zcxr{7ECi@SNk2|9r>CWGD{>&Fqmc#mq5?PYs9wGx#|9}1ooY_6+xV)9FR0|vLB%vh
zH7}m@l`h{DOjGGpYPH^BttB+ea(0wKqv=A5Nq0)Jhv}^*z8bl9)CLj3mWihh^zHv2
zh*yM6{Dyc+JY9UMgA@<|0T2KI5C8!X009sH0T2KI5cqBiynlZQb8WFDt0)QxW#9`_}O3w8P4V
z)Aazbcbkqir|U6(HBE4D#p!zFLQ?`)_y2#55wE|wgMyMl
z00ck)1V8`;KmY_l00ck)1V8`;zCMBP;m&Q0xOFE0ul@NSd++Nj19AcZ5C8!X009sH
T0T2KI5C8!X009vA{}cEJ#;W`-
literal 0
HcmV?d00001