Passwords: add and edit logins and notes, delete to trash, password generator, text Send
build-container / build (push) Successful in 6m41s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-29 11:10:47 -04:00
1 parent 70dc75d688
commit 4752aca43c
2 files changed
+149 -10

No files matched your search

+91 -4
View File
@@ -48,10 +48,13 @@ function vtUnlock(st, need2fa) {
async function vtList(st) {
const v = $('#view');
v.innerHTML = `<div class="wrap"><div class="ptop"><h1>Passwords</h1><span class="pspace grow"></span><button class="btn ghost" id="vtsync" aria-label="Refresh from the server" title="Refresh">${icon('refresh')}</button><button class="btn ghost" id="vtlock">Lock</button></div>
v.innerHTML = `<div class="wrap"><div class="ptop"><h1>Passwords</h1><span class="pspace grow"></span><button class="btn" id="vtnew">${icon('plus')}New</button><button class="btn ghost" id="vtgen">Generate</button><button class="btn ghost" id="vtsend">Send</button><button class="btn ghost" id="vtsync" aria-label="Refresh from the server" title="Refresh">${icon('refresh')}</button><button class="btn ghost" id="vtlock">Lock</button></div>
<input type="search" id="vtq" placeholder="Search your vault" value="${esc(vtState.q)}" autocomplete="off" aria-label="Search your vault" style="width:100%;margin-bottom:8px"><div id="vtfo"></div><div id="vtl"><p class="empty loading" role="status">Loading…</p></div>
<p class="sub">Locks itself after a few idle minutes. <a href="${esc(vtSafeUrl(st.url) || '#')}" target="_blank" rel="noopener" style="color:var(--accent)">Open the full vault</a> to add or change entries.</p></div>`;
<p class="sub">Locks itself after a few idle minutes. <a href="${esc(vtSafeUrl(st.url) || '#')}" target="_blank" rel="noopener" style="color:var(--accent)">Open the full vault</a> for cards, identities and the trash.</p></div>`;
const lockedOut = () => { vtState.items = null; vtUnlock({ ...st, unlocked: false }); };
$('#vtnew').onclick = () => vtEdit(null, () => load(), lockedOut);
$('#vtgen').onclick = () => vtGenerate();
$('#vtsend').onclick = () => vtSend(lockedOut);
$('#vtlock').onclick = async () => { try { await vtReq('/lock', {}); } catch {} lockedOut(); toast('Vault locked'); };
$('#vtsync').onclick = async () => { try { await vtReq('/sync', {}); await load(); toast('Refreshed'); } catch (e) { e.locked ? lockedOut() : toast(e.message); } };
let timer; $('#vtq').oninput = e => { vtState.q = e.target.value; clearTimeout(timer); timer = setTimeout(load, 250); };
@@ -64,12 +67,12 @@ async function vtList(st) {
$('#vtfo').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); vtState.folder = a.dataset.f; draw(); });
const rows = vtState.items.filter(i => !vtState.folder || i.folderId === vtState.folder);
$('#vtl').innerHTML = rows.length ? `<div class="list">${rows.map(i => `<button class="row" data-id="${esc(i.id)}"><span class="ic" style="background:${i.type === 1 ? '#1f6fb2' : i.type === 3 ? '#b25a1f' : '#6a7280'}">${icon(i.type === 1 ? 'key' : i.type === 3 ? 'store' : 'doc')}</span><span class="grow"><b>${esc(i.name)}</b><small>${esc(i.username || vtHost(i.uri) || vtType[i.type] || '')}</small></span>${i.favorite ? '<span aria-label="Favorite">★</span>' : ''}</button>`).join('')}</div>` : `<p class="empty">${vtState.q ? 'Nothing matches.' : 'Your vault is empty.'}</p>`;
$('#vtl').querySelectorAll('.row').forEach(b => b.onclick = () => vtOpen(b.dataset.id, lockedOut));
$('#vtl').querySelectorAll('.row').forEach(b => b.onclick = () => vtOpen(b.dataset.id, lockedOut, load));
}
if (vtState.items) draw(); load();
}
async function vtOpen(id, lockedOut) {
async function vtOpen(id, lockedOut, reload) {
let it; try { it = await vtReq('/item/' + encodeURIComponent(id)); } catch (e) { return e.locked ? lockedOut() : toast(e.message); }
const field = (label, value, key, secret) => value ? `<div class="vtf" style="margin:10px 0"><small style="color:var(--muted)">${esc(label)}</small><div style="display:flex;gap:8px;align-items:center"><span class="grow vtv" data-k="${key}" style="word-break:break-all;font-family:${secret ? 'ui-monospace,monospace' : 'inherit'}">${secret ? '••••••••••' : esc(value)}</span>${secret ? `<button class="btn ghost vtshow" data-k="${key}">Show</button>` : ''}<button class="btn ghost vtcp" data-k="${key}" data-w="${esc(label)}">Copy</button></div></div>` : '';
const vals = { user: it.username, pass: it.password, totp: it.totp };
@@ -81,7 +84,91 @@ async function vtOpen(id, lockedOut) {
${(it.fields || []).map((f, n) => field(f.name || 'Field', f.value, 'f' + n, f.hidden)).join('')}
${links.map(u => `<div class="vtf" style="margin:10px 0"><small style="color:var(--muted)">Website</small><div><a href="${esc(u)}" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">${esc(u)}</a></div></div>`).join('')}
${it.notes ? `<div class="vtf" style="margin:10px 0"><small style="color:var(--muted)">Notes</small><div style="white-space:pre-wrap">${esc(it.notes)}</div></div>` : ''}`);
if (it.type === 1 || it.type === 2) el.insertAdjacentHTML('beforeend', '<div class="bar"><button class="btn ghost" id="vted" style="flex:1;justify-content:center">Edit</button></div>');
if ($('#vted')) $('#vted').onclick = () => { closeSheet(); vtEdit(it, reload, lockedOut); };
Object.assign(vals, { chold: it.card && it.card.holder, cexp: it.card && [it.card.expMonth, it.card.expYear].filter(Boolean).join('/') });
el.querySelectorAll('.vtcp').forEach(b => b.onclick = () => vtCopy(vals[b.dataset.k], b.dataset.w));
el.querySelectorAll('.vtshow').forEach(b => b.onclick = () => { const s = el.querySelector(`.vtv[data-k="${b.dataset.k}"]`), on = b.textContent === 'Show'; s.textContent = on ? vals[b.dataset.k] : '••••••••••'; b.textContent = on ? 'Hide' : 'Show'; });
}
/** Make a random password or passphrase. */
function vtGenerate() {
const o = { type: 'password', length: 16, upper: 1, lower: 1, number: 1, special: 1, words: 4, capitalize: 1, inum: 0 };
const el = openSheet('Generate a password', 'Made on the server by the Bitwarden tool. Nothing is saved.', `<div class="seg"><a href="#" data-t="password" class="on">Password</a><a href="#" data-t="passphrase">Passphrase</a></div>
<div id="vgv" style="font-family:ui-monospace,monospace;word-break:break-all;font-size:18px;padding:12px;border-radius:10px;background:var(--panel2);margin:8px 0;min-height:28px"></div>
<div class="bar" style="margin:0 0 8px"><button class="btn" id="vgc" style="flex:1;justify-content:center">Copy</button><button class="btn ghost" id="vgr" style="flex:1;justify-content:center">New one</button></div>
<div id="vgo"></div><p class="err" id="vge" role="alert"></p>`);
let value = '';
const gen = async () => {
const q = new URLSearchParams(o.type === 'password' ? { type: 'password', length: o.length, upper: o.upper, lower: o.lower, number: o.number, special: o.special } : { type: 'passphrase', words: o.words, capitalize: o.capitalize, number: o.inum });
try { value = (await vtReq('/generate?' + q)).value; el.querySelector('#vgv').textContent = value; el.querySelector('#vge').textContent = ''; } catch (e) { el.querySelector('#vge').textContent = e.message; }
};
const opts = () => {
el.querySelector('#vgo').innerHTML = o.type === 'password'
? `<label for="vgl">Length: <b>${o.length}</b></label><input type="range" id="vgl" min="8" max="64" value="${o.length}" style="width:100%">${[['upper', 'A-Z'], ['lower', 'a-z'], ['number', '0-9'], ['special', '!@#$']].map(([k, l]) => `<label style="display:inline-flex;gap:6px;align-items:center;margin:8px 14px 0 0"><input type="checkbox" data-k="${k}" ${o[k] ? 'checked' : ''}>${l}</label>`).join('')}`
: `<label for="vgw">Words: <b>${o.words}</b></label><input type="range" id="vgw" min="3" max="10" value="${o.words}" style="width:100%"><label style="display:inline-flex;gap:6px;align-items:center;margin:8px 14px 0 0"><input type="checkbox" data-k="capitalize" ${o.capitalize ? 'checked' : ''}>Capitalize</label><label style="display:inline-flex;gap:6px;align-items:center;margin:8px 0 0"><input type="checkbox" data-k="inum" ${o.inum ? 'checked' : ''}>Add a number</label>`;
const r = el.querySelector('#vgl') || el.querySelector('#vgw'); r.oninput = () => { o[o.type === 'password' ? 'length' : 'words'] = +r.value; r.previousElementSibling.querySelector('b').textContent = r.value; }; r.onchange = gen;
el.querySelectorAll('#vgo input[type=checkbox]').forEach(c => c.onchange = () => { o[c.dataset.k] = c.checked ? 1 : 0; gen(); });
};
el.querySelectorAll('.seg a').forEach(a => a.onclick = e => { e.preventDefault(); o.type = a.dataset.t; el.querySelectorAll('.seg a').forEach(x => x.classList.toggle('on', x === a)); opts(); gen(); });
el.querySelector('#vgc').onclick = () => value && vtCopy(value, 'Password'); el.querySelector('#vgr').onclick = gen;
opts(); gen();
}
/** Bitwarden Send: a link that shows some text once (or a few times) and then goes away. */
async function vtSend(lockedOut) {
const el = openSheet('Send', 'Share some text through a private link that deletes itself.', `<form id="vsf" style="display:grid;gap:8px" autocomplete="off">
<label for="vsn">Name (only you see it)</label><input type="text" id="vsn" maxlength="100" placeholder="Wi-Fi password for the sitter">
<label for="vst">Text to share</label><textarea id="vst" rows="4" maxlength="10000" required></textarea>
<div class="bar" style="margin:0"><label style="flex:1">Deletes after<select id="vsd" style="width:100%"><option value="1">1 day</option><option value="7" selected>7 days</option><option value="31">31 days</option></select></label>
<label style="flex:1">Max views<input type="number" id="vsm" min="0" max="9999" value="1" style="width:100%" aria-describedby="vsmh"></label></div><small id="vsmh" style="color:var(--muted)">0 for no limit on views.</small>
<label for="vsp">Password to open it (optional)</label><input type="text" id="vsp" autocomplete="off" maxlength="200">
<p class="err" id="vse" role="alert"></p><button class="btn" id="vsg" style="justify-content:center">Make link</button></form><div id="vsr"></div><h2 style="margin-top:18px">Your sends</h2><div id="vsl"><p class="empty loading">Loading…</p></div>`);
const drawList = async () => {
try { const r = await vtReq('/sends'); el.querySelector('#vsl').innerHTML = r.sends.length ? `<div class="list">${r.sends.map(x => `<div class="row"><span class="grow"><b>${esc(x.name)}</b><small>${x.max ? x.views + ' of ' + x.max + ' views' : x.views + ' views'}${x.expires ? ' · deletes ' + esc(new Date(x.expires).toLocaleDateString()) : ''}</small></span><button class="btn ghost" data-c="${esc(x.url)}">Copy link</button><button class="btn ghost" data-d="${esc(x.id)}" aria-label="Delete ${esc(x.name)}">Delete</button></div>`).join('')}</div>` : '<p class="empty">No sends yet.</p>';
el.querySelectorAll('[data-c]').forEach(b => b.onclick = () => vtCopy(b.dataset.c, 'Link'));
el.querySelectorAll('[data-d]').forEach(b => b.onclick = async () => { try { await vtReq('/sends/delete', { id: b.dataset.d }); drawList(); } catch (e) { toast(e.message); } }); }
catch (e) { if (e.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vsl').innerHTML = `<p class="err">${esc(e.message)}</p>`; }
};
el.querySelector('#vsf').onsubmit = async e => {
e.preventDefault(); el.querySelector('#vse').textContent = ''; el.querySelector('#vsg').disabled = true;
try { const r = await vtReq('/sends', { name: el.querySelector('#vsn').value, text: el.querySelector('#vst').value, days: el.querySelector('#vsd').value, max: el.querySelector('#vsm').value, password: el.querySelector('#vsp').value });
el.querySelector('#vst').value = ''; el.querySelector('#vsp').value = '';
el.querySelector('#vsr').innerHTML = `<div class="card" style="margin-top:10px"><small style="color:var(--muted)">Your link</small><div style="word-break:break-all;margin:4px 0 8px">${esc(r.url)}</div><button class="btn" id="vscp" style="justify-content:center">Copy link</button></div>`;
el.querySelector('#vscp').onclick = () => vtCopy(r.url, 'Link'); vtCopy(r.url, 'Link'); drawList(); }
catch (err) { if (err.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vse').textContent = err.message; }
el.querySelector('#vsg').disabled = false;
};
drawList();
}
/** Add an entry (it === null) or change one. Logins and secure notes; cards and identities are changed on the Vaultwarden site. */
function vtEdit(it, reload, lockedOut) {
const isNew = !it, type = it ? it.type : 1;
const el = openSheet(isNew ? 'New entry' : 'Edit ' + (it.name || 'entry'), '', `<form id="vef" style="display:grid;gap:8px" autocomplete="off">
${isNew ? '<div class="seg" id="vet"><a href="#" data-t="1" class="on">Login</a><a href="#" data-t="2">Secure note</a></div>' : ''}
<label for="ven">Name</label><input type="text" id="ven" maxlength="1000" required value="${esc(it ? it.name : '')}">
<div id="vel">
<label for="veu">Username</label><input type="text" id="veu" autocapitalize="off" autocomplete="off" value="${esc(it ? it.username : '')}">
<label for="vep">Password</label><div style="display:flex;gap:8px"><input type="text" id="vep" class="grow" autocapitalize="off" autocomplete="new-password" style="font-family:ui-monospace,monospace" value="${esc(it ? it.password : '')}"><button type="button" class="btn ghost" id="veg">Generate</button></div>
<label for="vew">Website</label><input type="text" id="vew" autocapitalize="off" placeholder="https://" value="${esc(it ? it.uris[0] || '' : '')}">
<label for="vek">Authenticator key (optional)</label><input type="text" id="vek" autocapitalize="off" autocomplete="off" value="${esc(it ? it.totpSeed : '')}">
</div>
<label for="vex">Notes</label><textarea id="vex" rows="3" maxlength="10000">${esc(it ? it.notes : '')}</textarea>
<label for="vef2">Folder</label><select id="vef2"><option value="">No folder</option>${vtState.folders.map(f => `<option value="${esc(f.id)}" ${it && it.folderId === f.id ? 'selected' : ''}>${esc(f.name)}</option>`).join('')}</select>
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" id="vefv" ${it && it.favorite ? 'checked' : ''}>Favorite</label>
<p class="err" id="vee" role="alert"></p>
<div class="bar" style="margin:0"><button class="btn" id="ves" style="flex:1;justify-content:center">Save</button>${isNew ? '' : '<button type="button" class="btn ghost" id="ved" style="flex:1;justify-content:center">Delete</button>'}</div></form>`);
let t = type; const sync = () => { el.querySelector('#vel').hidden = t === 2; };
el.querySelectorAll('#vet a').forEach(a => a.onclick = e => { e.preventDefault(); t = +a.dataset.t; el.querySelectorAll('#vet a').forEach(x => x.classList.toggle('on', x === a)); sync(); }); sync();
if (it && it.type !== 1) el.querySelector('#vel').hidden = true;
el.querySelector('#veg').onclick = async () => { try { el.querySelector('#vep').value = (await vtReq('/generate?type=password&length=20&upper=1&lower=1&number=1&special=1')).value; } catch (e) { toast(e.message); } };
el.querySelector('#vef').onsubmit = async e => {
e.preventDefault(); el.querySelector('#vee').textContent = ''; el.querySelector('#ves').disabled = true;
const q = id => el.querySelector(id).value;
try { await vtReq('/item', { id: it && it.id, type: t, name: q('#ven'), username: q('#veu'), password: q('#vep'), uris: [q('#vew')], totp: q('#vek'), notes: q('#vex'), folderId: q('#vef2'), favorite: el.querySelector('#vefv').checked });
closeSheet(); toast(isNew ? 'Added' : 'Saved'); reload && reload(); }
catch (err) { if (err.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vee').textContent = err.message; el.querySelector('#ves').disabled = false; }
};
const del = el.querySelector('#ved'); if (del) del.onclick = () => confirmSheet('Delete this entry?', 'It moves to the vault\'s trash on the Vaultwarden site, where it can be restored.', 'Delete', async () => { try { await vtReq('/item/delete', { id: it.id }); toast('Deleted'); reload && reload(); } catch (err) { toast(err.message); } });
}