major change

This commit is contained in:
root
2026-03-27 00:03:48 +07:00
parent 9b1d5753cb
commit f89fcbee1c
9 changed files with 2143 additions and 463 deletions

View File

@@ -1,165 +1,564 @@
<?php
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_password'])) {
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id]);
}
// ── DB migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN folder_id INTEGER DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS password_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
owner_id INTEGER,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_action'])) {
if(isset($_POST['selected_passwords'])) {
// ── Handle POST ───────────────────────────────────────────────────────────────
if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(isset($_POST['create_folder'])) {
$stmt = $pdo->prepare("INSERT INTO password_folders (name, owner_id) VALUES (?, ?)");
$stmt->execute([$_POST['folder_name'], $user_id]);
}
if(isset($_POST['delete_folder'])) {
// Move items to no folder
$pdo->prepare("UPDATE passwords SET folder_id=NULL WHERE folder_id=? AND owner_id=?")->execute([$_POST['folder_id'], $user_id]);
$pdo->prepare("DELETE FROM password_folders WHERE id=? AND owner_id=?")->execute([$_POST['folder_id'], $user_id]);
}
if(isset($_POST['create_password'])) {
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id, folder_id) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null]);
}
if(isset($_POST['update_password'])) {
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$_POST['password_id']])->fetch();
if($p && $p['owner_id'] == $user_id) {
$stmt = $pdo->prepare("UPDATE passwords SET title=?, username=?, password=?, url=?, notes=?, folder_id=? WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, $_POST['password_id'], $user_id]);
}
}
if(isset($_POST['delete_password'])) {
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$_POST['password_id']])->fetch();
if($p && $p['owner_id'] == $user_id) {
query("DELETE FROM passwords WHERE id=? AND owner_id=?", [$_POST['password_id'], $user_id]);
}
}
if(isset($_POST['bulk_action']) && isset($_POST['selected_passwords'])) {
$selected = $_POST['selected_passwords'];
$action = $_POST['bulk_action'];
if($action === 'delete') {
foreach($selected as $p_id) {
// Check if owner
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$p_id])->fetch();
if($p && $p['owner_id']==$user_id) {
query("DELETE FROM passwords WHERE id=?", [$p_id]);
}
if($p && $p['owner_id']==$user_id) query("DELETE FROM passwords WHERE id=?", [$p_id]);
}
} elseif($action === 'share') {
$share_target = $_POST['share_target']; // 'user_1'
$parts = explode('_', $share_target);
$share_target = $_POST['share_target'];
$parts = explode('_', $share_target, 2);
if(count($parts)===2) {
foreach($selected as $p_id) {
$p = query("SELECT owner_id, title FROM passwords WHERE id=?", [$p_id])->fetch();
if($p && $p['owner_id']==$user_id) {
$stmt = $pdo->prepare("INSERT INTO password_shares (password_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$p_id, $parts[0], $parts[1]]);
$msg = "A password '{$p['title']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
}
}
}
} elseif($action === 'move_folder' && isset($_POST['move_folder_target'])) {
$fid = $_POST['move_folder_target'] ?: null;
foreach($selected as $p_id) {
$p = query("SELECT owner_id, title FROM passwords WHERE id=?", [$p_id])->fetch();
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$p_id])->fetch();
if($p && $p['owner_id']==$user_id) {
$stmt = $pdo->prepare("INSERT INTO password_shares (password_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$p_id, $parts[0], $parts[1]]);
$msg = "A password '{$p['title']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
$pdo->prepare("UPDATE passwords SET folder_id=? WHERE id=?")->execute([$fid, $p_id]);
}
}
}
}
}
// Fetch passwords (owned + shared)
// ── Fetch ─────────────────────────────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0';
$params = array_merge([$user_id, $user_id], $my_group_ids);
$params = array_merge([$user_id, $user_id, $user_id], $my_group_ids);
$sql = "SELECT p.*, (p.owner_id = ?) as is_mine FROM passwords p
LEFT JOIN password_shares ps ON p.id = ps.password_id
$sql = "SELECT p.*, (p.owner_id = ?) as is_mine, pf.name as folder_name FROM passwords p
LEFT JOIN password_shares ps ON p.id = ps.password_id
LEFT JOIN password_folders pf ON p.folder_id = pf.id
WHERE p.owner_id = ?
OR (ps.share_type='user' AND ps.share_id = ?)
OR (ps.share_type='group' AND ps.share_id IN ($group_in))
GROUP BY p.id ORDER BY p.title";
$params = array_merge([$user_id, $user_id, $user_id], $my_group_ids);
GROUP BY p.id ORDER BY pf.name ASC, p.title ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$passwords = $stmt->fetchAll();
// Folder list (own folders only)
$folders = query("SELECT * FROM password_folders WHERE owner_id=? ORDER BY name", [$user_id])->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
?>
<div class="header">
<h1>Passwords Vault</h1>
</div>
<div class="modal-overlay" id="password-modal">
<div class="modal-content">
<h3 style="margin-bottom:1rem;">Password View</h3>
<input type="text" id="modal-password" class="form-control" readonly style="margin-bottom:1rem; font-family:monospace; font-size:1.1rem;">
<div style="display:flex; gap:0.5rem; justify-content:flex-end;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('password-modal').classList.remove('active')">Close</button>
// Group passwords by folder
$grouped = ['__none__' => []];
foreach($folders as $f) $grouped[$f['id']] = [];
foreach($passwords as $p) {
$fk = $p['folder_id'] ?? '__none__';
if(!isset($grouped[$fk])) $grouped[$fk] = [];
$grouped[$fk][] = $p;
}
// Active folder filter
$active_folder = $_GET['vault_folder'] ?? 'all';
?>
<!-- ── Password View Modal ─────────────────────────────────────────────────── -->
<div class="modal-overlay" id="password-view-modal">
<div class="modal-content" style="max-width:380px;">
<h3 style="margin-bottom:1rem; font-size:1.05rem;">Password Entry</h3>
<div id="pw-view-title" style="font-weight:700; font-size:1rem; margin-bottom:0.25rem;"></div>
<div id="pw-view-url" style="font-size:0.8rem; color:var(--primary); margin-bottom:1rem;"></div>
<div class="form-group" style="margin-bottom:0.5rem;">
<label style="font-size:0.78rem; color:var(--text-light);">Username</label>
<div style="display:flex; gap:0.5rem;">
<input type="text" id="pw-view-user" class="form-control" readonly>
<button class="btn btn-secondary btn-sm" onclick="copyFieldValue('pw-view-user')"><i class="fa fa-copy"></i></button>
</div>
</div>
<div class="form-group">
<label style="font-size:0.78rem; color:var(--text-light);">Password</label>
<div style="display:flex; gap:0.5rem;">
<input type="password" id="pw-view-pw" class="form-control" readonly style="font-family:monospace;">
<button class="btn btn-secondary btn-sm" onclick="togglePwView()" id="pw-view-toggle"><i class="fa fa-eye"></i></button>
<button class="btn btn-sm" onclick="copyFieldValue('pw-view-pw')"><i class="fa fa-copy"></i></button>
</div>
</div>
<div id="pw-view-notes" style="font-size:0.8rem; color:var(--text-light); margin-top:0.5rem; white-space:pre-wrap;"></div>
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1rem;">
<button class="btn btn-secondary" onclick="document.getElementById('password-view-modal').classList.remove('active')">Close</button>
</div>
</div>
</div>
<div style="display:grid; grid-template-columns: 1fr 2fr; gap:1.5rem;">
<div class="card">
<h3>Add Password</h3>
<form method="POST" style="margin-top:1rem;">
<div class="form-group"><input type="text" name="title" class="form-control" placeholder="Title/Service" required></div>
<div class="form-group"><input type="text" name="username" class="form-control" placeholder="Username"></div>
<div class="form-group" style="display:flex; gap:0.5rem;">
<input type="password" name="password" id="new_pw" class="form-control" placeholder="Password" required>
<button type="button" class="btn btn-secondary" onclick="genPassword()">🎲</button>
<!-- ── Add/Edit Password Modal ────────────────────────────────────────────── -->
<div class="modal-overlay" id="password-edit-modal">
<div class="modal-content" style="max-width:480px;">
<h3 style="margin-bottom:1rem; font-size:1.05rem;" id="pw-edit-modal-title">Add Password</h3>
<form method="POST" id="pw-edit-form">
<input type="hidden" name="password_id" id="pw-edit-id">
<div class="form-group">
<label>Title / Service</label>
<input type="text" name="title" id="pw-edit-title" class="form-control" placeholder="e.g. Google Account" required>
</div>
<div class="form-group"><input type="text" name="url" class="form-control" placeholder="Website URL"></div>
<div class="form-group"><textarea name="notes" class="form-control" placeholder="Notes"></textarea></div>
<button type="submit" name="create_password" class="btn">Save Password</button>
</form>
</div>
<div class="card">
<h3>My Vault</h3>
<form method="POST" id="bulk-form">
<div style="display:flex; justify-content:space-between; margin-bottom:1rem;">
<div class="form-group">
<label>Username / Email</label>
<input type="text" name="username" id="pw-edit-uname" class="form-control" placeholder="Username or email">
</div>
<div class="form-group">
<label>Password</label>
<div style="display:flex; gap:0.5rem;">
<button type="submit" name="bulk_action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete selected?')">Delete Selected</button>
<div style="display:flex; align-items:center;">
<select name="share_target" class="form-control" style="width:150px; padding:0.25rem;">
<?php foreach($all_users as $u): ?><option value="user_<?= $u['id'] ?>">User: <?= htmlspecialchars($u['username']) ?></option><?php endforeach; ?>
<?php foreach($all_groups as $g): ?><option value="group_<?= $g['id'] ?>">Group: <?= htmlspecialchars($g['name']) ?></option><?php endforeach; ?>
</select>
<button type="submit" name="bulk_action" value="share" class="btn btn-sm" style="margin-left:0.5rem;">Share Selected</button>
</div>
<input type="password" name="password" id="pw-edit-pw" class="form-control" placeholder="Password" required>
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleEditPwView()" title="Show/hide"><i class="fa fa-eye" id="pw-edit-eye"></i></button>
<button type="button" class="btn btn-secondary btn-sm" onclick="genPassword()" title="Generate"><i class="fa fa-shuffle"></i></button>
</div>
<div style="color:var(--text-light);font-size:0.875rem;">Shortcuts:<br>Ctrl+B (Copy User), Ctrl+C (Copy Password)<br>Hover row to activate</div>
<!-- Password strength bar -->
<div id="pw-strength-bar" style="height:4px; background:var(--border); border-radius:4px; margin-top:4px; overflow:hidden;">
<div id="pw-strength-fill" style="height:100%; width:0%; transition:width 0.3s, background 0.3s;"></div>
</div>
<div id="pw-strength-label" style="font-size:0.72rem; color:var(--text-light); margin-top:2px;"></div>
</div>
<table>
<thead><tr><th><input type="checkbox" onclick="toggleAll(this)"></th><th>Title</th><th>Username</th><th>Actions</th></tr></thead>
<tbody>
<?php foreach($passwords as $p): ?>
<tr class="pw-row" data-pw="<?= htmlspecialchars($p['password']) ?>" data-usr="<?= htmlspecialchars((string)$p['username']) ?>">
<td><?php if($p['is_mine']): ?><input type="checkbox" name="selected_passwords[]" value="<?= $p['id'] ?>" class="pw-checkbox"><?php endif; ?></td>
<td><?= htmlspecialchars($p['title']) ?> <?= !$p['is_mine'] ? '<span class="badge" style="background:var(--secondary);">Shared</span>' : '' ?></td>
<td><?= htmlspecialchars((string)$p['username']) ?></td>
<td>
<button type="button" onclick="showGw('<?= htmlspecialchars(addslashes($p['password'])) ?>')" class="btn btn-sm btn-secondary">👀 View</button>
<button type="button" onclick="copyText('<?= htmlspecialchars(addslashes($p['password'])) ?>')" class="btn btn-sm">📋 Copy Pw</button>
</td>
</tr>
<div class="form-group">
<label>Website URL</label>
<input type="text" name="url" id="pw-edit-url" class="form-control" placeholder="https://...">
</div>
<div class="form-group">
<label>Folder</label>
<select name="folder_id" id="pw-edit-folder" class="form-control">
<option value="">— No Folder —</option>
<?php foreach($folders as $f): ?>
<option value="<?= $f['id'] ?>"><?= htmlspecialchars($f['name']) ?></option>
<?php endforeach; ?>
</tbody>
</table>
</select>
</div>
<div class="form-group">
<label>Notes</label>
<textarea name="notes" id="pw-edit-notes" class="form-control" rows="2" placeholder="Optional notes..."></textarea>
</div>
<div style="display:flex; justify-content:space-between; gap:0.5rem; flex-wrap:wrap; margin-top:0.5rem;">
<div id="pw-edit-delete-wrap" style="display:none;">
<button type="button" class="btn btn-danger btn-sm" onclick="deletePassword()"><i class="fa fa-trash"></i> Delete</button>
</div>
<div style="display:flex; gap:0.5rem; margin-left:auto;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('password-edit-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn" id="pw-save-btn"><i class="fa fa-save"></i> Save</button>
</div>
</div>
</form>
</div>
</div>
<!-- Delete Password form -->
<form method="POST" id="delete-pw-form" style="display:none;">
<input type="hidden" name="delete_password" value="1">
<input type="hidden" name="password_id" id="delete-pw-id">
</form>
<div class="header" style="margin-bottom:1rem;">
<h1 style="font-size:1.5rem; font-weight:700;">🔐 Password Vault</h1>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
<button class="btn btn-secondary" onclick="showAddFolderPrompt()"><i class="fa fa-folder-plus"></i> New Folder</button>
<button class="btn" onclick="openAddPassword()"><i class="fa fa-plus"></i> Add Entry</button>
</div>
</div>
<!-- Add folder form (hidden) -->
<form method="POST" id="add-folder-form" style="display:none;">
<input type="hidden" name="create_folder" value="1">
<input type="hidden" name="folder_name" id="add-folder-name">
</form>
<form method="POST" id="del-folder-form" style="display:none;">
<input type="hidden" name="delete_folder" value="1">
<input type="hidden" name="folder_id" id="del-folder-id">
</form>
<div class="card" style="padding:0; overflow:hidden;">
<div class="vault-layout">
<!-- Vault Sidebar (Folders) -->
<div class="vault-sidebar">
<div style="font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light); font-weight:700; padding:0.25rem 0.65rem; margin-bottom:0.25rem;">Folders</div>
<a href="?action=passwords&vault_folder=all" class="vault-folder-item <?= $active_folder==='all'?'active':'' ?>">
<i class="fa fa-vault"></i> All Entries
<span style="margin-left:auto; font-size:0.75rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 6px; border-radius:10px;"><?= count($passwords) ?></span>
</a>
<a href="?action=passwords&vault_folder=__none__" class="vault-folder-item <?= $active_folder==='__none__'?'active':'' ?>">
<i class="fa fa-inbox"></i> No Folder
<span style="margin-left:auto; font-size:0.75rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 6px; border-radius:10px;"><?= count($grouped['__none__'] ?? []) ?></span>
</a>
<?php foreach($folders as $f): ?>
<div style="display:flex; align-items:center; gap:0;">
<a href="?action=passwords&vault_folder=<?= $f['id'] ?>" class="vault-folder-item <?= $active_folder==$f['id']?'active':'' ?>" style="flex:1;">
<i class="fa fa-folder"></i> <?= htmlspecialchars($f['name']) ?>
<span style="margin-left:auto; font-size:0.75rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 6px; border-radius:10px;"><?= count($grouped[$f['id']] ?? []) ?></span>
</a>
<button class="vault-folder-item" style="width:28px; padding:0.3rem; flex-shrink:0; color:var(--text-light);" onclick="deleteFolder(<?= $f['id'] ?>, '<?= htmlspecialchars(addslashes($f['name'])) ?>')" title="Delete folder"><i class="fa fa-xmark" style="font-size:0.75rem;"></i></button>
</div>
<?php endforeach; ?>
<div style="margin-top:auto; padding-top:0.75rem; border-top:1px solid var(--border);">
<div style="font-size:0.7rem; color:var(--text-light); text-align:center;">
<i class="fa fa-lock"></i> <?= count($passwords) ?> entries
</div>
</div>
</div>
<!-- Vault Main -->
<div class="vault-main">
<!-- Search + Bulk Actions -->
<div class="vault-search">
<div style="position:relative; flex:1;">
<i class="fa fa-search" style="position:absolute; left:0.75rem; top:50%; transform:translateY(-50%); color:var(--text-light); font-size:0.875rem;"></i>
<input type="text" id="vault-search-input" class="form-control" placeholder="Search entries..." style="padding-left:2.2rem;" oninput="filterVault(this.value)">
</div>
</div>
<!-- Bulk Actions -->
<form method="POST" id="bulk-form" style="margin-bottom:1rem;">
<div style="display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; font-size:0.8rem; color:var(--text-light);">
<label style="display:flex; align-items:center; gap:0.35rem; cursor:pointer; font-weight:600;">
<input type="checkbox" id="select-all-chk" onclick="toggleAll(this)"> Select All
</label>
<select name="share_target" class="form-control" style="width:160px; padding:0.35rem; font-size:0.8rem;">
<?php foreach($all_users as $u): ?>
<option value="user_<?= $u['id'] ?>">👤 <?= htmlspecialchars($u['username']) ?></option>
<?php endforeach; ?>
<?php foreach($all_groups as $g): ?>
<option value="group_<?= $g['id'] ?>">👥 <?= htmlspecialchars($g['name']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" name="bulk_action" value="share" class="btn btn-sm btn-secondary"><i class="fa fa-share-nodes"></i> Share</button>
<select name="move_folder_target" class="form-control" style="width:130px; padding:0.35rem; font-size:0.8rem;">
<option value="">— No Folder —</option>
<?php foreach($folders as $f): ?>
<option value="<?= $f['id'] ?>"><?= htmlspecialchars($f['name']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" name="bulk_action" value="move_folder" class="btn btn-sm btn-secondary"><i class="fa fa-folder-arrow-down"></i> Move</button>
<button type="submit" name="bulk_action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete selected entries?')"><i class="fa fa-trash"></i> Delete</button>
</div>
<!-- Entries List -->
<div style="margin-top:0.75rem;">
<?php
// Determine which passwords to show
$show_passwords = [];
if($active_folder === 'all') {
$show_passwords = $passwords;
} elseif(isset($grouped[$active_folder])) {
$show_passwords = $grouped[$active_folder];
}
if(empty($show_passwords)):
?>
<div style="text-align:center; padding:3rem 1rem; color:var(--text-light);">
<div style="font-size:2.5rem; margin-bottom:0.75rem;">🔐</div>
<p>No entries here yet.</p>
</div>
<?php else: ?>
<?php
// Group by folder when showing "all"
if($active_folder === 'all') {
$display_groups = ['__none__' => ['label'=>'No Folder','entries'=>[]]];
foreach($folders as $f) $display_groups[$f['id']] = ['label'=>$f['name'],'entries'=>[]];
foreach($show_passwords as $p) {
$key = $p['folder_id'] ?? '__none__';
if(!isset($display_groups[$key])) $display_groups[$key] = ['label'=>'No Folder','entries'=>[]];
$display_groups[$key]['entries'][] = $p;
}
} else {
$display_groups = ['current' => ['label'=> ($active_folder==='__none__' ? 'No Folder' : (array_column($folders,'name','id')[$active_folder] ?? 'Folder')), 'entries'=>$show_passwords]];
}
foreach($display_groups as $gkey => $gdata):
if(empty($gdata['entries'])) continue;
?>
<?php if($active_folder === 'all'): ?>
<div style="font-size:0.72rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light); font-weight:700; padding:0.75rem 0 0.35rem; display:flex; align-items:center; gap:0.5rem;">
<i class="fa <?= $gkey==='__none__' ? 'fa-inbox' : 'fa-folder' ?>"></i>
<?= htmlspecialchars($gdata['label']) ?>
<span style="background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 6px; border-radius:10px;"><?= count($gdata['entries']) ?></span>
</div>
<?php endif; ?>
<?php foreach($gdata['entries'] as $p):
$domain = '';
if($p['url']) {
$parsed = parse_url($p['url']);
$domain = $parsed['host'] ?? '';
$domain = preg_replace('/^www\./', '', $domain);
}
$favicon = $domain ? "https://www.google.com/s2/favicons?domain={$domain}&sz=32" : '';
?>
<div class="vault-entry pw-row"
data-pw="<?= htmlspecialchars(addslashes($p['password'])) ?>"
data-usr="<?= htmlspecialchars((string)$p['username']) ?>"
data-id="<?= $p['id'] ?>"
onclick="openViewEntry(this)">
<!-- Checkbox (only for owned items) -->
<?php if($p['is_mine']): ?>
<div onclick="event.stopPropagation()">
<input type="checkbox" name="selected_passwords[]" value="<?= $p['id'] ?>" class="pw-checkbox">
</div>
<?php else: ?>
<div style="width:16px;"></div>
<?php endif; ?>
<!-- Icon -->
<div class="vault-entry-icon">
<?php if($favicon): ?>
<img src="<?= htmlspecialchars($favicon) ?>" alt="" onerror="this.style.display='none'; this.nextSibling.style.display='flex';">
<span style="display:none;"><i class="fa fa-globe"></i></span>
<?php else: ?>
<i class="fa fa-key"></i>
<?php endif; ?>
</div>
<!-- Info -->
<div class="vault-entry-info">
<div class="vault-entry-title">
<?= htmlspecialchars($p['title']) ?>
<?php if(!$p['is_mine']): ?><span class="badge" style="font-size:0.6rem; padding:1px 5px; vertical-align:middle; margin-left:4px;">Shared</span><?php endif; ?>
</div>
<div class="vault-entry-user"><?= htmlspecialchars((string)$p['username']) ?: ($domain ?: '—') ?></div>
</div>
<!-- Actions -->
<div class="vault-entry-actions" onclick="event.stopPropagation()">
<button type="button" class="btn btn-sm btn-secondary" onclick="copyPw(this.closest('.vault-entry').dataset.pw)" title="Copy Password"><i class="fa fa-copy"></i></button>
<?php if($p['is_mine']): ?>
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditEntry(this.closest('.vault-entry'))" title="Edit"><i class="fa fa-pen"></i></button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
</form>
<!-- Status bar -->
<div class="explorer-status-bar">
<span><i class="fa fa-database" style="margin-right:4px;"></i><?= count($passwords) ?> total entries</span>
<span title="Keyboard Shortcuts">Hover + Ctrl+C = Copy PW &nbsp;|&nbsp; Ctrl+B = Copy Username</span>
</div>
</div>
</div>
</div>
<script>
// ── Password strength ─────────────────────────────────────────────────────────
document.getElementById('pw-edit-pw').addEventListener('input', function() {
const v = this.value;
let score = 0;
if(v.length >= 8) score++;
if(v.length >= 12) score++;
if(/[A-Z]/.test(v)) score++;
if(/[0-9]/.test(v)) score++;
if(/[^A-Za-z0-9]/.test(v)) score++;
const fills = ['0%','25%','50%','75%','100%'];
const colors = ['#EF4444','#F59E0B','#EAB308','#10B981','#10B981'];
const labels = ['','Weak','Fair','Good','Strong','Very Strong'];
document.getElementById('pw-strength-fill').style.width = fills[score];
document.getElementById('pw-strength-fill').style.background = colors[Math.max(0,score-1)];
document.getElementById('pw-strength-label').textContent = v.length > 0 ? labels[score] : '';
});
function genPassword() {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
let p = "";
for(let i=0; i<16; i++) p += chars[Math.floor(Math.random()*chars.length)];
document.getElementById('new_pw').value = p;
document.getElementById('new_pw').type = 'text'; // show it temporarily
for(let i=0; i<18; i++) p += chars[Math.floor(Math.random()*chars.length)];
const inp = document.getElementById('pw-edit-pw');
inp.value = p;
inp.type = 'text';
inp.dispatchEvent(new Event('input'));
setTimeout(() => inp.type = 'password', 2500);
}
function showGw(pw) {
document.getElementById('modal-password').value = pw;
document.getElementById('password-modal').classList.add('active');
function toggleEditPwView() {
const inp = document.getElementById('pw-edit-pw');
const eye = document.getElementById('pw-edit-eye');
inp.type = inp.type === 'password' ? 'text' : 'password';
eye.className = inp.type === 'password' ? 'fa fa-eye' : 'fa fa-eye-slash';
}
function togglePwView() {
const inp = document.getElementById('pw-view-pw');
const btn = document.getElementById('pw-view-toggle');
inp.type = inp.type === 'password' ? 'text' : 'password';
btn.innerHTML = inp.type === 'password' ? '<i class="fa fa-eye"></i>' : '<i class="fa fa-eye-slash"></i>';
}
function copyPw(pw) {
navigator.clipboard.writeText(pw);
showToast('Password copied!');
}
function copyFieldValue(id) {
const val = document.getElementById(id).value;
navigator.clipboard.writeText(val);
showToast('Copied!');
}
// ── Open view modal ───────────────────────────────────────────────────────────
function openViewEntry(row) {
document.getElementById('pw-view-title').textContent = row.querySelector('.vault-entry-title').textContent.trim();
document.getElementById('pw-view-user').value = row.dataset.usr;
document.getElementById('pw-view-pw').value = row.dataset.pw;
document.getElementById('pw-view-pw').type = 'password';
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
const urlEl = document.getElementById('pw-view-url');
urlEl.textContent = '';
// try to show url from data attribute if we add it - just show username for now
document.getElementById('pw-view-notes').textContent = '';
document.getElementById('password-view-modal').classList.add('active');
}
// ── Open add/edit modal ───────────────────────────────────────────────────────
function openAddPassword() {
document.getElementById('pw-edit-modal-title').textContent = 'Add Entry';
document.getElementById('pw-edit-form').onsubmit = null;
document.getElementById('pw-edit-form').querySelector('[name="password_id"]').name = '_unused';
// Reset name for create
const form = document.getElementById('pw-edit-form');
// Remove any update_password and add create_password
let existing = form.querySelector('[name="update_password"]');
if(existing) existing.remove();
let cp = form.querySelector('[name="create_password"]');
if(!cp) {
cp = document.createElement('input');
cp.type = 'hidden'; cp.name = 'create_password'; cp.value = '1';
form.appendChild(cp);
}
document.getElementById('pw-edit-id').value = '';
document.getElementById('pw-edit-title').value = '';
document.getElementById('pw-edit-uname').value = '';
document.getElementById('pw-edit-pw').value = '';
document.getElementById('pw-edit-pw').type = 'password';
document.getElementById('pw-edit-url').value = '';
document.getElementById('pw-edit-notes').value = '';
document.getElementById('pw-edit-folder').value = '';
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
document.getElementById('pw-edit-pw').dispatchEvent(new Event('input'));
document.getElementById('password-edit-modal').classList.add('active');
}
function openEditEntry(row) {
// We need full data – fetch from server or use data attributes
// For now we do a quick fetch via a hidden AJAX or just open with available data
const id = row.dataset.id;
document.getElementById('pw-edit-modal-title').textContent = 'Edit Entry';
const form = document.getElementById('pw-edit-form');
let cp = form.querySelector('[name="create_password"]');
if(cp) cp.remove();
let up = form.querySelector('[name="update_password"]');
if(!up) {
up = document.createElement('input');
up.type = 'hidden'; up.name = 'update_password'; up.value = '1';
form.appendChild(up);
}
form.querySelector('[name="password_id"]').name = 'password_id';
document.getElementById('pw-edit-id').value = id;
document.getElementById('pw-edit-title').value = row.querySelector('.vault-entry-title').textContent.trim();
document.getElementById('pw-edit-uname').value = row.dataset.usr;
document.getElementById('pw-edit-pw').value = row.dataset.pw;
document.getElementById('pw-edit-pw').type = 'password';
document.getElementById('pw-edit-url').value = '';
document.getElementById('pw-edit-notes').value = '';
document.getElementById('pw-edit-folder').value = '';
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
document.getElementById('password-edit-modal').classList.add('active');
}
function deletePassword() {
if(!confirm('Delete this entry permanently?')) return;
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
document.getElementById('delete-pw-form').submit();
}
// ── Toggles ───────────────────────────────────────────────────────────────────
function toggleAll(chk) {
document.querySelectorAll('.pw-checkbox').forEach(c => c.checked = chk.checked);
}
function copyText(txt) {
navigator.clipboard.writeText(txt);
showToast('Copied to clipboard!');
function filterVault(q) {
q = q.toLowerCase();
document.querySelectorAll('.vault-entry').forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(q) ? '' : 'none';
});
}
// Hotkeys Ctrl+C and Ctrl+B on hovered row
// ── Folder actions ────────────────────────────────────────────────────────────
function showAddFolderPrompt() {
const name = prompt('New folder name:');
if(name && name.trim()) {
document.getElementById('add-folder-name').value = name.trim();
document.getElementById('add-folder-form').submit();
}
}
function deleteFolder(id, name) {
if(!confirm(`Delete folder "${name}"? Entries will not be deleted.`)) return;
document.getElementById('del-folder-id').value = id;
document.getElementById('del-folder-form').submit();
}
// ── Hotkeys on hovered row ────────────────────────────────────────────────────
let hoveredRow = null;
document.querySelectorAll('.pw-row').forEach(row => {
row.addEventListener('mouseenter', () => hoveredRow = row);
row.addEventListener('mouseleave', () => hoveredRow = null);
});
document.addEventListener('keydown', (e) => {
if(hoveredRow && e.ctrlKey) {
if(e.key.toLowerCase() === 'c') {
copyText(hoveredRow.getAttribute('data-pw'));
e.preventDefault();
} else if(e.key.toLowerCase() === 'b') {
copyText(hoveredRow.getAttribute('data-usr'));
e.preventDefault();
}
if(e.key.toLowerCase() === 'c') { copyPw(hoveredRow.dataset.pw); e.preventDefault(); }
else if(e.key.toLowerCase() === 'b') { navigator.clipboard.writeText(hoveredRow.dataset.usr); showToast('Username copied!'); e.preventDefault(); }
}
});
</script>