485 lines
31 KiB
JavaScript
485 lines
31 KiB
JavaScript
const API = '/password-manager/api.php';
|
|
let token = sessionStorage.getItem('authToken');
|
|
let curUser = sessionStorage.getItem('currentUsername');
|
|
let view = localStorage.getItem('vaultView') || 'grid';
|
|
let showView = localStorage.getItem('showViewBtn') !== 'false';
|
|
let showMail = localStorage.getItem('showEmail') !== 'false';
|
|
let dark = localStorage.getItem('darkTheme') !== 'false';
|
|
let lockMin = parseInt(localStorage.getItem('autoLockMinutes') || '5');
|
|
let order = JSON.parse(localStorage.getItem('entryOrder') || '[]');
|
|
let selectedFolder = localStorage.getItem('selectedFolder') || 'All';
|
|
let entries = [];
|
|
let folders = ['All']; // Will be populated from server
|
|
let genPwdVal = '';
|
|
let cryptoKey = null;
|
|
let idleT, warnT, countT;
|
|
let draggedId = null;
|
|
|
|
// ==================== TOAST ====================
|
|
function toast(m, t) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.textContent = m; c.appendChild(d); setTimeout(() => d.remove(), 3000); }
|
|
function showZigzagToast(element, message, type) {
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast-zigzag ' + (type || 'success');
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
const rect = element.getBoundingClientRect();
|
|
toast.style.left = rect.left + 'px';
|
|
toast.style.top = rect.top + 'px';
|
|
setTimeout(() => toast.remove(), 1500);
|
|
}
|
|
|
|
// ==================== THEME ====================
|
|
function applyTheme() { document.body.classList.toggle('light', !dark); }
|
|
function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark); applyTheme(); }
|
|
|
|
// ==================== CRYPTO ====================
|
|
function checkStrength() { const p = document.getElementById('passwordInput').value; const b = document.getElementById('strengthBar'); let s = 0; if (p.length >= 8) s++; if (p.length >= 12) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/\d/.test(p)) s++; if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; b.className = 'strength-bar s' + Math.min(4, s); }
|
|
async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); }
|
|
async function encryptPwd(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plain)); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(enc))), iv: btoa(String.fromCharCode(...iv)) }; }
|
|
async function decryptPwd(encB64, ivB64) { try { const enc = Uint8Array.from(atob(encB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, enc); return new TextDecoder().decode(dec); } catch (e) { return '[ERROR]'; } }
|
|
|
|
// ==================== AUTO-LOCK ====================
|
|
function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); }
|
|
function resetIdle() { clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); document.getElementById('idleWarning').classList.remove('show'); if (lockMin > 0 && token) { const lm = lockMin * 60000; warnT = setTimeout(() => { document.getElementById('idleWarning').classList.add('show'); let cd = 30; document.getElementById('idleCountdown').textContent = cd; countT = setInterval(() => { cd--; document.getElementById('idleCountdown').textContent = cd; if (cd <= 0) { clearInterval(countT); doLogout(); } }, 1000); }, Math.max(0, lm - 30000)); idleT = setTimeout(() => doLogout(), lm); } }
|
|
|
|
// ==================== USERNAME ====================
|
|
function saveUsername() { const f = document.getElementById('usernameInput'); if (f && f.value.trim()) localStorage.setItem('savedUsername', f.value.trim()); }
|
|
function loadUsername() { const s = localStorage.getItem('savedUsername'); const f = document.getElementById('usernameInput'); if (s && f) f.value = s; }
|
|
|
|
// ==================== FOLDERS (server-backed) ====================
|
|
async function loadFolders() {
|
|
try {
|
|
const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } });
|
|
if (r.ok) {
|
|
folders = await r.json();
|
|
if (!folders.includes('All')) folders.unshift('All');
|
|
} else {
|
|
folders = ['All'];
|
|
}
|
|
} catch (e) {
|
|
folders = ['All'];
|
|
}
|
|
}
|
|
|
|
async function addFolderToServer(name) {
|
|
try {
|
|
const r = await fetch(API + '/folders', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
body: JSON.stringify({ name })
|
|
});
|
|
if (r.ok) {
|
|
await loadFolders();
|
|
return true;
|
|
} else {
|
|
const d = await r.json();
|
|
toast('❌ ' + (d.error || 'Error'), 'error');
|
|
return false;
|
|
}
|
|
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
|
}
|
|
|
|
async function deleteFolderFromServer(name) {
|
|
try {
|
|
const r = await fetch(API + '/folders/' + encodeURIComponent(name), {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
if (r.ok) {
|
|
await loadFolders();
|
|
if (selectedFolder === name) {
|
|
selectedFolder = 'All';
|
|
localStorage.setItem('selectedFolder', 'All');
|
|
}
|
|
return true;
|
|
} else {
|
|
const d = await r.json();
|
|
toast('❌ ' + (d.error || 'Error'), 'error');
|
|
return false;
|
|
}
|
|
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
|
|
}
|
|
|
|
function renderFolders() {
|
|
const bar = document.getElementById('foldersBar');
|
|
if (!bar) return;
|
|
|
|
// Ensure every entry has a string folder
|
|
entries.forEach(e => {
|
|
if (!e.folder || typeof e.folder !== 'string') e.folder = 'All';
|
|
});
|
|
|
|
const counts = {};
|
|
entries.forEach(e => {
|
|
const f = e.folder;
|
|
counts[f] = (counts[f] || 0) + 1;
|
|
});
|
|
|
|
let html = '';
|
|
folders.forEach(f => {
|
|
const count = counts[f] || 0;
|
|
html += `<span class="folder-chip${selectedFolder === f ? ' active' : ''}" onclick="selectFolder('${esc(f)}')">📁 ${esc(f)}<span class="folder-count">${count}</span>${f !== 'All' ? `<button class="folder-delete-btn" onclick="event.stopPropagation();showDeleteFolderConfirm('${esc(f)}')">✕</button>` : ''}</span>`;
|
|
});
|
|
html += `<button class="folder-add-btn" onclick="showAddFolderModal()">+ New</button>`;
|
|
bar.innerHTML = html;
|
|
}
|
|
|
|
function populateAddFolderSelect() {
|
|
const select = document.getElementById('addFolderSelect');
|
|
if (!select) return;
|
|
select.innerHTML = '';
|
|
folders.forEach(f => {
|
|
const option = document.createElement('option');
|
|
option.value = f;
|
|
option.textContent = '📁 ' + f;
|
|
if (f === selectedFolder) option.selected = true;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function selectFolder(f) {
|
|
selectedFolder = f;
|
|
localStorage.setItem('selectedFolder', f);
|
|
renderFolders();
|
|
populateAddFolderSelect();
|
|
render();
|
|
}
|
|
|
|
function showAddFolderModal() {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'custom-modal-overlay show';
|
|
overlay.innerHTML = `
|
|
<div class="custom-modal">
|
|
<h3>📁 New Folder</h3>
|
|
<input type="text" id="newFolderName" placeholder="Folder name">
|
|
<div class="modal-actions">
|
|
<button class="btn btn-outline btn-sm" id="cancelAddFolder">Cancel</button>
|
|
<button class="btn btn-sm" id="confirmAddFolder">Create</button>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(overlay);
|
|
document.getElementById('cancelAddFolder').onclick = () => overlay.remove();
|
|
document.getElementById('confirmAddFolder').onclick = async () => {
|
|
const name = document.getElementById('newFolderName').value.trim();
|
|
if (!name) { toast('Enter a name', 'error'); return; }
|
|
const success = await addFolderToServer(name);
|
|
if (success) {
|
|
renderFolders();
|
|
populateAddFolderSelect();
|
|
overlay.remove();
|
|
toast('📁 Folder created!');
|
|
}
|
|
};
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
}
|
|
|
|
function showDeleteFolderConfirm(folderName) {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'custom-modal-overlay show';
|
|
overlay.innerHTML = `
|
|
<div class="custom-modal">
|
|
<h3>🗑️ Delete Folder</h3>
|
|
<p style="color:var(--text2);margin-bottom:1rem;">Delete "${folderName}"? Entries will move to "All".</p>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-outline btn-sm" id="cancelDeleteFolder">Cancel</button>
|
|
<button class="btn btn-sm btn-danger" id="confirmDeleteFolder">Delete</button>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(overlay);
|
|
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
|
|
document.getElementById('confirmDeleteFolder').onclick = async () => {
|
|
const success = await deleteFolderFromServer(folderName);
|
|
if (success) {
|
|
renderFolders();
|
|
populateAddFolderSelect();
|
|
render();
|
|
overlay.remove();
|
|
toast('📁 Folder deleted');
|
|
}
|
|
};
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
}
|
|
|
|
// ==================== INIT ====================
|
|
function init() {
|
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
|
document.getElementById('showViewBtnToggle').classList.toggle('active', showView);
|
|
document.getElementById('showEmailToggle').classList.toggle('active', showMail);
|
|
document.getElementById('autoLockTimer').value = lockMin;
|
|
applyTheme();
|
|
document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
|
loadUsername();
|
|
const sl = localStorage.getItem('savedLoginUser');
|
|
if (sl) document.getElementById('loginUsername').value = sl;
|
|
document.getElementById('viewToggle').addEventListener('click', e => {
|
|
if (e.target.classList.contains('view-btn')) {
|
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
view = e.target.dataset.view;
|
|
localStorage.setItem('vaultView', view);
|
|
render();
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); document.getElementById('showViewBtnToggle').classList.toggle('active', showView); render(); }
|
|
function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); document.getElementById('showEmailToggle').classList.toggle('active', showMail); document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; render(); }
|
|
|
|
// ==================== GENERATOR ====================
|
|
function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); }
|
|
function closeGen() { document.getElementById('genModal').style.display = 'none'; }
|
|
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
|
|
function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; for (let i = 0; i < l; i++) p += c.charAt(Math.floor(Math.random() * c.length)); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
|
|
function useGen() { if (!genPwdVal) genPwd(); document.getElementById('passwordInput').value = genPwdVal; checkStrength(); navigator.clipboard.writeText(genPwdVal); toast('🎲 Copied!'); closeGen(); }
|
|
|
|
// ==================== AUTH ====================
|
|
function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); }
|
|
|
|
async function login() {
|
|
const u = document.getElementById('loginUsername').value.trim();
|
|
const p = document.getElementById('loginPassword').value;
|
|
if (!u || !p) { toast('Fill all fields', 'error'); return; }
|
|
document.getElementById('loginBtn').disabled = true;
|
|
localStorage.setItem('savedLoginUser', u);
|
|
try {
|
|
const r = await fetch(API + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
|
|
const d = await r.json();
|
|
if (r.ok) {
|
|
token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt);
|
|
sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', u);
|
|
await loadFolders();
|
|
toast('✅ Login!'); showVault(); loadEntries();
|
|
} else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; }
|
|
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
|
finally { document.getElementById('loginBtn').disabled = false; }
|
|
}
|
|
|
|
async function register() {
|
|
const u = document.getElementById('regUsername').value.trim();
|
|
const p = document.getElementById('regPassword').value;
|
|
if (u.length < 3) { toast('Username min 3', 'error'); return; }
|
|
if (p.length < 8) { toast('Password min 8', 'error'); return; }
|
|
document.getElementById('registerBtn').disabled = true;
|
|
try {
|
|
const r = await fetch(API + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
|
|
const d = await r.json();
|
|
if (r.ok) {
|
|
token = d.token; curUser = u; cryptoKey = await deriveKey(p, d.salt);
|
|
sessionStorage.setItem('authToken', token); sessionStorage.setItem('currentUsername', u);
|
|
await loadFolders();
|
|
toast('✅ Created!'); showVault(); loadEntries();
|
|
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
|
} catch (e) { toast('⚠️ Connection error', 'error'); }
|
|
finally { document.getElementById('registerBtn').disabled = false; }
|
|
}
|
|
|
|
function doLogout() {
|
|
saveUsername();
|
|
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
|
|
document.getElementById('idleWarning').classList.remove('show');
|
|
token = null; curUser = null; entries = []; cryptoKey = null; folders = ['All'];
|
|
sessionStorage.clear();
|
|
document.getElementById('authSection').classList.remove('hidden');
|
|
document.getElementById('vaultSection').classList.add('hidden');
|
|
document.getElementById('loginPassword').value = '';
|
|
document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
|
|
}
|
|
|
|
function showVault() {
|
|
document.getElementById('authSection').classList.add('hidden');
|
|
document.getElementById('vaultSection').classList.remove('hidden');
|
|
document.getElementById('currentUser').textContent = '👤 ' + curUser;
|
|
document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
|
document.getElementById('autoLockTimer').value = lockMin;
|
|
loadUsername();
|
|
renderFolders();
|
|
populateAddFolderSelect();
|
|
resetIdle();
|
|
}
|
|
|
|
// ==================== ENTRIES ====================
|
|
function applyOrder(list) { if (!list || !list.length) return []; if (!order || !order.length) return list; const map = new Map(list.filter(e => e && e.id).map(e => [e.id, e])); const ord = []; order.forEach(id => { if (map.has(id)) { ord.push(map.get(id)); map.delete(id); } }); map.forEach(e => ord.push(e)); return ord; }
|
|
|
|
async function loadEntries(q) {
|
|
try {
|
|
let url = API + '/entries';
|
|
if (q) url += '?search=' + encodeURIComponent(q);
|
|
const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } });
|
|
if (r.ok) {
|
|
const raw = await r.json();
|
|
entries = [];
|
|
for (const e of raw) {
|
|
if (e.encryption_method === 'client') {
|
|
const pw = await decryptPwd(e.encrypted_password, e.iv);
|
|
entries.push({ id: e.id, site: e.site, username: e.username, password: pw, folder: e.folder || 'All' });
|
|
} else {
|
|
entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All' });
|
|
}
|
|
}
|
|
entries = applyOrder(entries);
|
|
document.getElementById('connectionStatus').textContent = '🟢 Connected';
|
|
document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)';
|
|
renderFolders();
|
|
populateAddFolderSelect();
|
|
render();
|
|
} else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); }
|
|
} catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); }
|
|
}
|
|
|
|
function getFilteredEntries() {
|
|
if (selectedFolder === 'All') return entries;
|
|
return entries.filter(e => (e.folder || 'All') === selectedFolder);
|
|
}
|
|
|
|
function render() {
|
|
const c = document.getElementById('entriesContainer');
|
|
c.className = '';
|
|
c.classList.add(view + '-view');
|
|
const filtered = getFilteredEntries();
|
|
if (!filtered.length) { c.innerHTML = '<div style="text-align:center;color:var(--text2);padding:2rem;grid-column:1/-1">📭 No entries</div>'; return; }
|
|
if (view === 'table') {
|
|
let h = '<table><thead><tr><th>Site</th>' +
|
|
(showMail ? '<th>User</th>' : '') +
|
|
'<th>Password</th><th>Folder</th><th>Actions</th></tr></thead><tbody>';
|
|
filtered.forEach(e => {
|
|
h += '<tr class="table-row-drag" draggable="true" data-id="' + e.id + '">' +
|
|
'<td>🌐 ' + esc(e.site) + '</td>' +
|
|
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
|
|
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>' +
|
|
'<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
|
'<td class="actions-cell">' +
|
|
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button> ' : '') +
|
|
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
|
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
|
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
|
'</td>' +
|
|
'</tr>';
|
|
});
|
|
h += '</tbody></table>';
|
|
c.innerHTML = h;
|
|
}else { c.innerHTML = filtered.map(e => (view === 'grid' ? gridC(e) : view === 'compact' ? compC(e) : listC(e))).join(''); }
|
|
attachEvents();
|
|
setupDrag();
|
|
}
|
|
|
|
function gridC(e) { return '<div class="entry-card" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div><div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span><div>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div></div>'; }
|
|
function listC(e) { return '<div class="entry-row" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div></div>'; }
|
|
function compC(e) { return '<div class="entry-compact" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span>🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span>👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>'; }
|
|
|
|
// ==================== EVENTS ====================
|
|
function showConfirm(btn, message, callback) {
|
|
const id = btn.dataset.id;
|
|
const existing = document.querySelector('.custom-confirm');
|
|
if (existing) existing.remove();
|
|
const confirm = document.createElement('div');
|
|
confirm.className = 'custom-confirm show';
|
|
confirm.innerHTML = '<div class="confirm-text">' + message + '</div><div class="confirm-btns"><button class="confirm-yes">Yes</button><button class="confirm-no">No</button></div>';
|
|
document.body.appendChild(confirm);
|
|
const rect = btn.getBoundingClientRect();
|
|
confirm.style.top = (rect.top - 60) + 'px';
|
|
let leftPos = rect.left - confirm.offsetWidth + rect.width;
|
|
if (leftPos < 10) leftPos = 10;
|
|
confirm.style.left = leftPos + 'px';
|
|
confirm.querySelector('.confirm-yes').onclick = () => { confirm.remove(); callback(id); showZigzagToast(btn, '🗑️ Deleted!', 'error'); };
|
|
confirm.querySelector('.confirm-no').onclick = () => confirm.remove();
|
|
setTimeout(() => { document.addEventListener('click', function closeConfirm(e) { if (!confirm.contains(e.target) && e.target !== btn) { confirm.remove(); document.removeEventListener('click', closeConfirm); } }); }, 10);
|
|
}
|
|
|
|
function attachEvents() {
|
|
document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); showConfirm(this, 'Delete this entry?', id => delEntry(id)); });
|
|
document.querySelectorAll('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); });
|
|
document.querySelectorAll('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); el.textContent = el.textContent === '••••••••' ? el.dataset.pw : '••••••••'; });
|
|
document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); try { await navigator.clipboard.writeText(el.dataset.pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } });
|
|
}
|
|
|
|
function setupDrag() {
|
|
const c = document.getElementById('entriesContainer'); if (!c) return;
|
|
c.querySelectorAll('[draggable="true"]').forEach(el => {
|
|
el.ondragstart = function(e) { draggedId = this.dataset.id; this.style.opacity = '0.5'; e.dataTransfer.setData('text/plain', this.dataset.id); e.dataTransfer.effectAllowed = 'move'; };
|
|
el.ondragend = function(e) { this.style.opacity = '1'; draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); };
|
|
el.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dataset.id !== draggedId) this.classList.add('drag-over'); };
|
|
el.ondragleave = function(e) { this.classList.remove('drag-over'); };
|
|
el.ondrop = function(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); const fromId = parseInt(e.dataTransfer.getData('text/plain')); const toId = parseInt(this.dataset.id); if (!fromId || !toId || fromId === toId) return; let fi = -1, ti = -1; for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id === fromId) fi = i; if (entries[i] && entries[i].id === toId) ti = i; } if (fi > -1 && ti > -1 && fi !== ti) { const moved = entries.splice(fi, 1)[0]; entries.splice(ti, 0, moved); order = entries.map(e => e.id); localStorage.setItem('entryOrder', JSON.stringify(order)); render(); } };
|
|
});
|
|
}
|
|
|
|
// ==================== EDIT ====================
|
|
function openEdit(id) {
|
|
let e = null;
|
|
for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id == id) { e = entries[i]; break; } }
|
|
if (!e) return;
|
|
const folderSelect = document.getElementById('editFolder');
|
|
folderSelect.innerHTML = '';
|
|
folders.forEach(f => {
|
|
const option = document.createElement('option');
|
|
option.value = f;
|
|
option.textContent = '📁 ' + f;
|
|
if (f === (e.folder || 'All')) option.selected = true;
|
|
folderSelect.appendChild(option);
|
|
});
|
|
document.getElementById('editId').value = id;
|
|
document.getElementById('editSite').value = e.site;
|
|
document.getElementById('editUsername').value = e.username;
|
|
document.getElementById('editPassword').value = e.password;
|
|
document.getElementById('editPassword').type = 'password';
|
|
document.getElementById('editModal').classList.add('show');
|
|
}
|
|
function closeEdit() { document.getElementById('editModal').classList.remove('show'); }
|
|
function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; }
|
|
|
|
async function saveEdit() {
|
|
const id = document.getElementById('editId').value;
|
|
const site = document.getElementById('editSite').value.trim();
|
|
const username = document.getElementById('editUsername').value.trim();
|
|
const password = document.getElementById('editPassword').value;
|
|
const folder = document.getElementById('editFolder').value;
|
|
if (!site || !password) { toast('Site and password required', 'error'); return; }
|
|
try {
|
|
const enc = await encryptPwd(password);
|
|
const r = await fetch(API + '/entries/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ site, username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) });
|
|
if (r.ok) { toast('✅ Updated!'); closeEdit(); loadEntries(); }
|
|
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
|
} catch (e) { toast('⚠️ Error', 'error'); }
|
|
}
|
|
|
|
// ==================== ADD / DELETE ====================
|
|
async function addEntry() {
|
|
const site = document.getElementById('siteInput').value.trim();
|
|
const user = document.getElementById('usernameInput').value.trim();
|
|
const pass = document.getElementById('passwordInput').value;
|
|
if (!site || !pass) { toast('❌ Site and password required', 'error'); return; }
|
|
if (user) localStorage.setItem('savedUsername', user);
|
|
document.getElementById('addBtn').disabled = true;
|
|
try {
|
|
const enc = await encryptPwd(pass);
|
|
const folder = document.getElementById('addFolderSelect').value;
|
|
const r = await fetch(API + '/entries', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, encryption_method: 'client', folder }) });
|
|
if (r.ok) { document.getElementById('siteInput').value = ''; document.getElementById('passwordInput').value = ''; document.getElementById('strengthBar').className = 'strength-bar s0'; toast('✅ Saved!'); loadEntries(); }
|
|
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
|
} catch (e) { toast('⚠️ Error', 'error'); }
|
|
finally { document.getElementById('addBtn').disabled = false; }
|
|
}
|
|
|
|
async function delEntry(id) { try { const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('🗑️ Deleted!'); loadEntries(); } } catch (e) { toast('Error', 'error'); } }
|
|
|
|
// ==================== UTILS ====================
|
|
function searchEntries() { loadEntries(document.getElementById('searchInput').value); }
|
|
function exportPasswords() { if (!confirm('Export plain text?')) return; const b = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = 'vault-' + new Date().toISOString().slice(0, 10) + '.json'; a.click(); URL.revokeObjectURL(a.href); toast('Exported!'); }
|
|
function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
|
|
|
|
// ==================== STARTUP ====================
|
|
init();
|
|
applyTheme();
|
|
if (token && curUser) {
|
|
// Re-derive key from session? Can't without password, so we need to re-authenticate.
|
|
// For now, just show vault if token exists but warn user they must log in again.
|
|
sessionStorage.clear();
|
|
token = null;
|
|
curUser = null;
|
|
document.getElementById('authSection').classList.remove('hidden');
|
|
document.getElementById('vaultSection').classList.add('hidden');
|
|
}
|
|
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
|
|
document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } });
|
|
document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
|
|
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); }); |