Files
Task-Management/views/passwords.php

694 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ── DB migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN folder_id INTEGER DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN tags TEXT 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, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? '')]);
notify_user($user_id, "You added a new password: " . $_POST['title'], ['action'=>'Created', 'module'=>'Password', 'item'=>$_POST['title']]);
}
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=?, tags=? WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? ''), $_POST['password_id'], $user_id]);
$ctx = ['action'=>'Updated', 'module'=>'Password', 'item'=>$_POST['title']];
notify_user($user_id, "Password '{$_POST['title']}' updated.", $ctx);
// Notify shared users/groups
$shares = query("SELECT share_type, share_id FROM password_shares WHERE password_id=?", [$_POST['password_id']])->fetchAll();
foreach($shares as $sh) {
if($sh['share_type'] === 'user') notify_user($sh['share_id'], "Shared password '{$_POST['title']}' updated.", $ctx);
else notify_group($sh['share_id'], "Shared group password '{$_POST['title']}' updated.", $ctx);
}
}
}
if(isset($_POST['delete_password'])) {
$p = query("SELECT owner_id, title FROM passwords WHERE id=?", [$_POST['password_id']])->fetch();
$ctx = ['action'=>'Deleted', 'module'=>'Password', 'item'=>$p['title']];
// Notify shared recipients before deletion
$shares = query("SELECT share_type, share_id FROM password_shares WHERE password_id=?", [$_POST['password_id']])->fetchAll();
foreach($shares as $sh) {
if($sh['share_type'] === 'user') notify_user($sh['share_id'], "Shared password '{$p['title']}' has been deleted.", $ctx);
else notify_group($sh['share_id'], "Shared group password '{$p['title']}' has been deleted.", $ctx);
}
query("DELETE FROM passwords WHERE id=? AND owner_id=?", [$_POST['password_id'], $user_id]);
notify_user($user_id, "Password '{$p['title']}' deleted.", $ctx);
}
// Unshare a specific share record
if(isset($_POST['remove_password_share'])) {
$share_id = (int)$_POST['share_id'];
$chk = query("SELECT ps.id FROM password_shares ps JOIN passwords p ON ps.password_id=p.id WHERE ps.id=? AND p.owner_id=?", [$share_id, $user_id])->fetch();
if($chk) query("DELETE FROM password_shares WHERE id=?", [$share_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, title FROM passwords WHERE id=?", [$p_id])->fetch();
if($p && $p['owner_id']==$user_id) {
query("DELETE FROM passwords WHERE id=?", [$p_id]);
notify_waha('Deleted', 'Password Entry', $p['title']);
}
}
} 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.";
$ctx = ['action'=>'Shared', 'module'=>'Password', 'item'=>$p['title'], 'user'=>$_SESSION['username']??'System'];
if($parts[0] === 'user') notify_user($parts[1], $msg, $ctx);
else notify_group($parts[1], $msg, $ctx);
}
}
}
} 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 id="pw-view-tags-container" style="display:none; flex-wrap:wrap; gap:0.25rem; margin-top:1rem; padding-top:0.5rem; border-top:1px dashed var(--border);"></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 class="form-group">
<label>Tags</label>
<input type="text" name="tags" id="pw-edit-tags" class="form-control tagify-input" placeholder="Add tags...">
</div>
<!-- Shares management section (populated by JS) -->
<div id="pw-shares-section" style="display:none; border-top:1px solid var(--border); padding-top:0.75rem; margin-top:0.5rem;">
<label style="font-size:0.8rem; color:var(--text-light); display:block; margin-bottom:0.4rem;">Shared with</label>
<div id="pw-shares-list" style="display:flex; flex-wrap:wrap; gap:0.35rem;"></div>
</div>
<div style="display:flex; justify-content:space-between; gap:0.5rem; flex-wrap:wrap; margin-top:0.75rem;">
<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>
<form method="POST" id="remove-share-form" style="display:none;">
<input type="hidden" name="remove_password_share" value="1">
<input type="hidden" name="share_id" id="remove-share-id">
</form>
<!-- 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 style="background:var(--surface); border-radius:1rem; box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border); overflow:hidden;">
<div class="vault-layout" style="min-height:calc(100vh - 130px);">
<!-- 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">
<div style="display:flex; justify-content:flex-end; gap:0.5rem; flex-wrap:wrap; margin-bottom:1rem;">
<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>
<!-- 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="button" class="btn btn-sm btn-danger" onclick="appConfirm('Delete selected entries?').then(y => { if(y) { const el = document.createElement('input'); el.type='hidden'; el.name='bulk_action'; el.value='delete'; this.form.appendChild(el); this.form.submit(); } })"><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" : '';
$pw_shares = [];
if($p['is_mine']) {
$pw_shares = query("SELECT ps.id, ps.share_type, ps.share_id, u.username, u.profile_pic as upic, g.name as gname, g.profile_pic as gpic FROM password_shares ps LEFT JOIN users u ON (ps.share_type='user' AND ps.share_id=u.id) LEFT JOIN groups g ON (ps.share_type='group' AND ps.share_id=g.id) WHERE ps.password_id=?", [$p['id']])->fetchAll();
}
$shares_json = htmlspecialchars(json_encode($pw_shares), ENT_QUOTES, 'UTF-8');
?>
<div class="vault-entry pw-row"
data-pw="<?= htmlspecialchars(addslashes($p['password'])) ?>"
data-usr="<?= htmlspecialchars((string)$p['username']) ?>"
data-id="<?= $p['id'] ?>"
data-url="<?= htmlspecialchars((string)($p['url'] ?? '')) ?>"
data-notes="<?= htmlspecialchars(addslashes((string)($p['notes'] ?? ''))) ?>"
data-folder-id="<?= htmlspecialchars((string)($p['folder_id'] ?? '')) ?>"
data-shares="<?= $shares_json ?>"
data-tags="<?= htmlspecialchars((string)($p['tags'] ?? '')) ?>"
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<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.createElement('div');
document.getElementById('pw-view-url').innerHTML = ''; // Clear existing
if(row.dataset.url) {
urlEl.innerHTML = `<a href="${row.dataset.url}" target="_blank" style="color:var(--primary);">${row.dataset.url}</a>`;
document.getElementById('pw-view-url').appendChild(urlEl);
}
document.getElementById('pw-view-notes').textContent = row.dataset.notes || '';
// Tag rendering
const tagsContainer = document.getElementById('pw-view-tags-container');
tagsContainer.innerHTML = '';
const tagsStr = row.dataset.tags || '';
if(tagsStr) {
tagsContainer.style.display = 'flex';
tagsStr.split(',').forEach(t => {
t = t.trim();
if(!t) return;
const span = document.createElement('span');
span.style.cssText = 'font-size:0.65rem; padding:2px 8px; border-radius:12px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-weight:600; display:inline-flex; align-items:center; gap:0.2rem;';
span.innerHTML = `<i class="fa fa-tag" style="font-size:0.6rem;"></i> ${t}`;
tagsContainer.appendChild(span);
});
} else {
tagsContainer.style.display = 'none';
}
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-tags').value = '';
document.getElementById('pw-shares-section').style.display = 'none';
document.getElementById('pw-shares-list').innerHTML = '';
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 = row.dataset.url || '';
document.getElementById('pw-edit-notes').value = row.dataset.notes || '';
document.getElementById('pw-edit-folder').value = row.dataset.folderId || '';
document.getElementById('pw-edit-tags').value = row.dataset.tags || '';
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
renderPasswordShares(row.dataset.shares || '[]');
document.getElementById('password-edit-modal').classList.add('active');
}
function renderPasswordShares(jsonShares) {
const sec = document.getElementById('pw-shares-section');
const list = document.getElementById('pw-shares-list');
list.innerHTML = '';
let shares = [];
try { shares = JSON.parse(jsonShares || '[]'); } catch(e) { shares = []; }
if(!shares || shares.length === 0) {
sec.style.display = 'none';
return;
}
shares.forEach(sh => {
const pic = sh.share_type === 'user' ? sh.upic : sh.gpic;
const name = sh.share_type === 'user' ? (sh.username || 'User') : (sh.gname || 'Group');
const chip = document.createElement('div');
chip.style.display = 'inline-flex';
chip.style.alignItems = 'center';
chip.style.gap = '0.4rem';
chip.style.fontSize = '0.72rem';
chip.style.padding = '2px 8px 2px 2px';
chip.style.borderRadius = '999px';
chip.style.background = 'rgba(var(--primary-rgb),0.1)';
chip.style.color = 'var(--primary)';
chip.style.border = '1px solid rgba(var(--primary-rgb),0.2)';
let avatarHtml = `<div style="width:20px; height:20px; border-radius:50%; background:var(--primary); color:white; display:flex; align-items:center; justify-content:center; font-size:0.6rem; font-weight:700; overflow:hidden;">${name.charAt(0).toUpperCase()}</div>`;
if(pic) {
avatarHtml = `<img src="${pic}" style="width:20px; height:20px; border-radius:50%; object-fit:cover;">`;
}
chip.innerHTML = `${avatarHtml}<span style="font-weight:600;">${name}</span><button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;margin-left:2px;font-size:0.9rem;" title="Unshare" onclick="removePasswordShare(${Number(sh.id)})">×</button>`;
list.appendChild(chip);
});
sec.style.display = 'block';
}
function removePasswordShare(shareId) {
if(!shareId) return;
appConfirm('Unshare this password access?').then(y => {
if(y) {
document.getElementById('remove-share-id').value = String(shareId);
document.getElementById('remove-share-form').submit();
}
});
}
function deletePassword() {
appConfirm('Delete this entry permanently?').then(y => {
if(y) {
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();
const tags = (row.dataset.tags || '').toLowerCase();
row.style.display = (text.includes(q) || tags.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) {
appConfirm(`Delete folder "${name}"? Entries will not be deleted.`).then(y => {
if(y) {
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>