565 lines
32 KiB
PHP
565 lines
32 KiB
PHP
<?php
|
||
// ── 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) {}
|
||
|
||
// ── 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) {
|
||
$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]);
|
||
}
|
||
} elseif($action === 'share') {
|
||
$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 FROM passwords WHERE id=?", [$p_id])->fetch();
|
||
if($p && $p['owner_id']==$user_id) {
|
||
$pdo->prepare("UPDATE passwords SET folder_id=? WHERE id=?")->execute([$fid, $p_id]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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, $user_id], $my_group_ids);
|
||
|
||
$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 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();
|
||
|
||
// 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>
|
||
|
||
<!-- ── 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">
|
||
<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;">
|
||
<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>
|
||
<!-- 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>
|
||
<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; ?>
|
||
</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 | 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<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 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 filterVault(q) {
|
||
q = q.toLowerCase();
|
||
document.querySelectorAll('.vault-entry').forEach(row => {
|
||
const text = row.textContent.toLowerCase();
|
||
row.style.display = text.includes(q) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
// ── 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') { copyPw(hoveredRow.dataset.pw); e.preventDefault(); }
|
||
else if(e.key.toLowerCase() === 'b') { navigator.clipboard.writeText(hoveredRow.dataset.usr); showToast('Username copied!'); e.preventDefault(); }
|
||
}
|
||
});
|
||
</script>
|