Das Speichern eines neuen Eintrags im Popup las den Token direkt aus chrome.storage.local und verlangte den manuellen apiToken. Nach einer SSO-Anmeldung wird dieser Schluessel jedoch geloescht (nur Refresh-Token plus kurzlebiger Access-Token in storage.session), sodass das Anlegen immer mit "Nicht konfiguriert." abbrach. Das Anlegen laeuft jetzt wie alle uebrigen Aufrufe ueber den Background-Service-Worker (apiFetch): SSO-Access-Token mit automatischer Erneuerung oder, falls gesetzt, manueller Token. Eine serverseitige Sperre (423) fuehrt zur PIN-Abfrage statt zu einer Fehlermeldung, und der Eintrags-Cache wird nach dem Anlegen verworfen. Ausserdem entfaellt in den Optionen ein redundanter Statusabruf, der ebenfalls nur den manuellen Token beruecksichtigte; loadConnStatus() deckt beide Anmeldewege bereits ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G12DRMpe4UjYDwuRU1sjy1
593 lines
No EOL
23 KiB
JavaScript
593 lines
No EOL
23 KiB
JavaScript
'use strict';
|
||
|
||
let allEntries = null;
|
||
let pageMatches = [];
|
||
let entryIndex = {}; // id -> entry
|
||
let detailState = null; // aktiver Eintrag im Detail-Panel
|
||
let selIndex = -1; // Tastatur-Auswahl in der Liste
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
async function init() {
|
||
chrome.storage.local.get(['serverUrl'], cfg => {
|
||
if (cfg.serverUrl) $('btnOpen').href = cfg.serverUrl + '/vault';
|
||
});
|
||
|
||
$('search').addEventListener('input', onSearch);
|
||
$('search').addEventListener('keydown', onListKeydown);
|
||
$('btnOptions').addEventListener('click', () => chrome.runtime.openOptionsPage());
|
||
$('btnNew').addEventListener('click', openNewPanel);
|
||
$('btnCloseNew').addEventListener('click', closeNewPanel);
|
||
$('btnSaveNew').addEventListener('click', saveNewEntry);
|
||
$('btnGenPw').addEventListener('click', generatePassword);
|
||
$('btnRevealNewPw').addEventListener('click', () => {
|
||
const f = $('nePassword');
|
||
f.type = f.type === 'password' ? 'text' : 'password';
|
||
});
|
||
|
||
// Detail-Panel
|
||
$('btnDetailBack').addEventListener('click', closeDetail);
|
||
$('btnDetailClose').addEventListener('click', closeDetail);
|
||
$('btnRevealUser').addEventListener('click', toggleRevealUser);
|
||
$('btnCopyUser').addEventListener('click', () => copySecret($('detailUser').dataset.value || '', 'Benutzername kopiert'));
|
||
$('btnRevealPass').addEventListener('click', toggleRevealPass);
|
||
$('btnCopyPass').addEventListener('click', copyDetailPassword);
|
||
$('btnCopyTotp').addEventListener('click', () => { const c = $('detailTotp').dataset.code || ''; if (c) copySecret(c, 'TOTP kopiert'); });
|
||
$('btnCopyNotes').addEventListener('click', () => copyToClipboard($('detailNotes').dataset.value || '', 'Notiz kopiert'));
|
||
$('btnDetailFill').addEventListener('click', fillActiveTab);
|
||
|
||
// Lock-Screen
|
||
$('lockSubmit').addEventListener('click', submitPin);
|
||
$('lockPin').addEventListener('keydown', e => { if (e.key === 'Enter') submitPin(); });
|
||
$('btnLock').addEventListener('click', lockNow);
|
||
|
||
boot();
|
||
}
|
||
|
||
// Reihenfolge: Status (App/User/PIN) → Lock prüfen → Liste oder PIN-Schirm.
|
||
function boot() {
|
||
chrome.runtime.sendMessage({ type: 'CHECK_STATUS' }, resp => {
|
||
if (resp?.ok) {
|
||
$('hdTitle').textContent = 'OpenNIT Vault';
|
||
// Untertitel: angemeldeter Nutzer und – zur Orientierung – die Instanz.
|
||
const parts = [];
|
||
if (resp.user) parts.push(resp.user);
|
||
if (resp.app_name) parts.push(resp.app_name);
|
||
$('hdUser').textContent = parts.join(' · ');
|
||
}
|
||
chrome.runtime.sendMessage({ type: 'GET_LOCK' }, lock => {
|
||
$('btnLock').style.display = lock?.required ? '' : 'none';
|
||
if (lock?.required && !lock.unlocked) {
|
||
showLockScreen();
|
||
} else {
|
||
hideLockScreen();
|
||
reload(true);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Lock-Screen ────────────────────────────────────────────────────────────
|
||
function showLockScreen() {
|
||
$('lockScreen').style.display = 'block';
|
||
$('listWrap').style.display = 'none';
|
||
$('newEntryPanel').style.display = 'none';
|
||
$('detailPanel').style.display = 'none';
|
||
$('search').closest('.search-wrap').style.display = 'none';
|
||
$('lockMsg').textContent = '';
|
||
$('lockPin').value = '';
|
||
setTimeout(() => $('lockPin').focus(), 50);
|
||
}
|
||
function hideLockScreen() {
|
||
$('lockScreen').style.display = 'none';
|
||
$('search').closest('.search-wrap').style.display = '';
|
||
}
|
||
function submitPin() {
|
||
const pin = $('lockPin').value;
|
||
if (!pin) { $('lockMsg').textContent = 'Bitte PIN eingeben.'; return; }
|
||
$('lockSubmit').disabled = true;
|
||
$('lockSubmit').textContent = '…';
|
||
$('lockMsg').textContent = '';
|
||
chrome.runtime.sendMessage({ type: 'DO_UNLOCK', pin }, resp => {
|
||
$('lockSubmit').disabled = false;
|
||
$('lockSubmit').textContent = 'Entsperren';
|
||
if (resp?.ok) {
|
||
hideLockScreen();
|
||
reload(true);
|
||
} else {
|
||
let m = resp?.error || 'PIN falsch.';
|
||
if (resp?.lockSecs > 0) m += ' (' + resp.lockSecs + 's gesperrt)';
|
||
$('lockMsg').textContent = m;
|
||
$('lockPin').value = '';
|
||
$('lockPin').focus();
|
||
}
|
||
});
|
||
}
|
||
function lockNow() {
|
||
chrome.runtime.sendMessage({ type: 'LOCK_NOW' }, () => showLockScreen());
|
||
}
|
||
|
||
function reload(force) {
|
||
closeDetailTimers();
|
||
detailState = null;
|
||
selIndex = -1;
|
||
$('search').value = '';
|
||
$('listWrap').innerHTML = '<div class="loading"><div class="spin"></div></div>';
|
||
$('listWrap').style.display = '';
|
||
$('newEntryPanel').style.display = 'none';
|
||
$('detailPanel').style.display = 'none';
|
||
$('search').closest('.search-wrap').style.display = '';
|
||
|
||
chrome.runtime.sendMessage({ type: 'GET_ENTRIES', force }, resp => {
|
||
// Serverseitig gesperrt (Token-Härtung) → PIN-Schirm zeigen.
|
||
if (resp?.locked) { showLockScreen(); return; }
|
||
allEntries = resp?.entries ?? null;
|
||
entryIndex = {};
|
||
if (allEntries === null) {
|
||
$('listWrap').innerHTML = '<div class="error">⚠ Nicht verbunden.<br>Einstellungen prüfen.</div>';
|
||
return;
|
||
}
|
||
indexEntries(allEntries);
|
||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||
const url = tabs[0]?.url;
|
||
if (url && !url.startsWith('chrome://') && !url.startsWith('chrome-extension://')) {
|
||
chrome.runtime.sendMessage({ type: 'GET_MATCHING_ENTRIES', url }, r2 => {
|
||
pageMatches = r2?.entries || [];
|
||
indexEntries(pageMatches);
|
||
renderDefault();
|
||
});
|
||
} else {
|
||
pageMatches = [];
|
||
renderDefault();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function indexEntries(list) {
|
||
(list || []).forEach(e => { entryIndex[String(e.id)] = e; });
|
||
}
|
||
|
||
function renderDefault() {
|
||
selIndex = -1;
|
||
if (pageMatches.length > 0) {
|
||
$('listWrap').innerHTML =
|
||
'<div class="section-lbl match">Passend für diese Seite</div>' +
|
||
'<div class="entries" id="eList">' + pageMatches.map(e => entryHtml(e)).join('') + '</div>';
|
||
} else {
|
||
$('listWrap').innerHTML =
|
||
'<div class="section-lbl">Alle Einträge (' + allEntries.length + ')</div>' +
|
||
'<div class="entries scrollable" id="eList">' +
|
||
(allEntries.length ? allEntries.map(e => entryHtml(e)).join('') : '<div class="empty">Noch keine Einträge vorhanden.</div>') +
|
||
'</div>';
|
||
}
|
||
const el = document.getElementById('eList');
|
||
if (el) attachHandlers(el);
|
||
}
|
||
|
||
function onSearch() {
|
||
selIndex = -1;
|
||
const q = ($('search').value || '').trim().toLowerCase();
|
||
if (!q) { renderDefault(); return; }
|
||
if (!allEntries) return;
|
||
|
||
const filtered = allEntries.filter(e =>
|
||
(e.title ||'').toLowerCase().includes(q) ||
|
||
(e.username ||'').toLowerCase().includes(q) ||
|
||
(e.url ||'').toLowerCase().includes(q) ||
|
||
(e.notes ||'').toLowerCase().includes(q) ||
|
||
(e.team_name||'').toLowerCase().includes(q)
|
||
);
|
||
|
||
$('listWrap').innerHTML =
|
||
'<div class="section-lbl">Suche (' + filtered.length + ')</div>' +
|
||
'<div class="entries scrollable" id="eList">' +
|
||
(filtered.length ? filtered.map(e => entryHtml(e)).join('') : '<div class="empty">Keine Einträge gefunden.</div>') +
|
||
'</div>';
|
||
|
||
const el = document.getElementById('eList');
|
||
if (el) attachHandlers(el);
|
||
}
|
||
|
||
// Tastatur-Navigation aus dem Suchfeld heraus (↑↓ wählt, Enter öffnet).
|
||
function onListKeydown(e) {
|
||
if (detailState || $('newEntryPanel').style.display === 'block') return;
|
||
const items = [...document.querySelectorAll('#eList .entry')];
|
||
if (!items.length) return;
|
||
if (e.key === 'ArrowDown') { e.preventDefault(); setSel(items, selIndex + 1); }
|
||
else if (e.key === 'ArrowUp') { e.preventDefault(); setSel(items, selIndex - 1); }
|
||
else if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const target = selIndex >= 0 ? items[selIndex] : items[0];
|
||
if (target) openDetail(target.dataset.id);
|
||
}
|
||
}
|
||
function setSel(items, idx) {
|
||
items.forEach(i => i.classList.remove('kbd-sel'));
|
||
selIndex = Math.max(0, Math.min(idx, items.length - 1));
|
||
const el = items[selIndex];
|
||
if (el) { el.classList.add('kbd-sel'); el.scrollIntoView({ block: 'nearest' }); }
|
||
}
|
||
|
||
// ── New Entry ─────────────────────────────────────────────────────────────
|
||
function openNewPanel() {
|
||
$('listWrap').style.display = 'none';
|
||
$('search').closest('.search-wrap').style.display = 'none';
|
||
$('detailPanel').style.display = 'none';
|
||
$('newEntryPanel').style.display = 'block';
|
||
$('neTitle').value = '';
|
||
$('neUsername').value = '';
|
||
$('nePassword').value = '';
|
||
$('nePassword').type = 'password';
|
||
$('neUrl').value = '';
|
||
$('neNotes').value = '';
|
||
$('newEntryMsg').textContent = '';
|
||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||
const url = tabs[0]?.url;
|
||
if (url && !url.startsWith('chrome://') && !url.startsWith('chrome-extension://')) {
|
||
$('neUrl').value = url;
|
||
}
|
||
});
|
||
$('neTitle').focus();
|
||
}
|
||
|
||
function closeNewPanel() {
|
||
$('newEntryPanel').style.display = 'none';
|
||
$('listWrap').style.display = '';
|
||
$('search').closest('.search-wrap').style.display = '';
|
||
}
|
||
|
||
function generatePassword() {
|
||
const len = 20;
|
||
const sets = [
|
||
'abcdefghijkmnopqrstuvwxyz',
|
||
'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||
'23456789',
|
||
'!@#$%^&*()-_=+[]{}',
|
||
];
|
||
const all = sets.join('');
|
||
const buf = new Uint32Array(len);
|
||
crypto.getRandomValues(buf);
|
||
let out = [];
|
||
// Mindestens ein Zeichen je Set
|
||
sets.forEach((s, i) => { out.push(s[buf[i] % s.length]); });
|
||
for (let i = sets.length; i < len; i++) out.push(all[buf[i] % all.length]);
|
||
// mischen
|
||
for (let i = out.length - 1; i > 0; i--) {
|
||
const j = buf[i] % (i + 1);
|
||
[out[i], out[j]] = [out[j], out[i]];
|
||
}
|
||
$('nePassword').value = out.join('');
|
||
$('nePassword').type = 'text';
|
||
}
|
||
|
||
// Speichern läuft – wie alle anderen Aufrufe – über den Background-Service-Worker,
|
||
// der den gültigen Zugang (SSO oder manueller Token) beisteuert.
|
||
function saveNewEntry() {
|
||
const title = $('neTitle').value.trim();
|
||
if (!title) { $('newEntryMsg').textContent = 'Titel ist erforderlich.'; return; }
|
||
|
||
$('btnSaveNew').disabled = true;
|
||
$('btnSaveNew').textContent = '...';
|
||
$('newEntryMsg').textContent = '';
|
||
|
||
const entry = {
|
||
title: title,
|
||
username: $('neUsername').value.trim(),
|
||
password: $('nePassword').value,
|
||
url: $('neUrl').value.trim(),
|
||
notes: $('neNotes').value.trim(),
|
||
};
|
||
|
||
chrome.runtime.sendMessage({ type: 'CREATE_ENTRY', entry }, resp => {
|
||
$('btnSaveNew').disabled = false;
|
||
$('btnSaveNew').textContent = 'Speichern';
|
||
if (resp?.ok) {
|
||
closeNewPanel();
|
||
reload(true);
|
||
showToast('Eintrag gespeichert');
|
||
return;
|
||
}
|
||
if (resp?.locked) { showLockScreen(); return; }
|
||
$('newEntryMsg').textContent = resp?.error || 'Fehler beim Speichern.';
|
||
});
|
||
}
|
||
|
||
// ── Liste (Klick öffnet Detailansicht) ─────────────────────────────────────
|
||
function monogram(title) {
|
||
const s = String(title || '?');
|
||
let h = 0;
|
||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;
|
||
const ch = s.charAt(0).toUpperCase().replace(/[&<>]/g, '');
|
||
return { hue: h, ch: ch };
|
||
}
|
||
|
||
function entryHtml(e) {
|
||
const userText = esc(e.username) || '<span style="color:#adb5bd;font-style:italic">Kein Benutzername</span>';
|
||
const m = monogram(e.title);
|
||
const icon = `<span class="entry-mono" style="display:inline-flex;width:20px;height:20px;border-radius:4px;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:hsl(${m.hue},52%,90%);color:hsl(${m.hue},55%,38%);">${m.ch}</span>`;
|
||
const totpBadge = e.has_totp ? '<span class="entry-2fa">2FA</span>' : '';
|
||
return `
|
||
<div class="entry" data-id="${e.id}" data-domain="${escAttr(e.favicon_domain)}">
|
||
<div class="entry-icon">${icon}</div>
|
||
<div class="entry-info">
|
||
<div class="entry-title">${esc(e.title)}${totpBadge}</div>
|
||
<div class="entry-user">${userText}</div>
|
||
<div class="entry-meta">
|
||
<div class="entry-url">${esc(e.url) || ''}</div>
|
||
${e.team_name ? `<span class="team-badge">${esc(e.team_name)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<svg class="entry-chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function attachHandlers(container) {
|
||
container.querySelectorAll('.entry').forEach(row => {
|
||
row.addEventListener('click', () => openDetail(row.dataset.id));
|
||
});
|
||
loadFavicons(container);
|
||
}
|
||
|
||
function loadFavicons(container) {
|
||
container.querySelectorAll('.entry[data-domain]').forEach(row => {
|
||
if (!row.dataset.domain) return;
|
||
const id = row.dataset.id;
|
||
chrome.runtime.sendMessage({ type: 'GET_FAVICON', id }, resp => {
|
||
if (resp?.dataUrl) {
|
||
const ic = row.querySelector('.entry-icon');
|
||
if (ic) ic.innerHTML = '<img src="' + resp.dataUrl + '" alt="">';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Detailansicht ─────────────────────────────────────────────────────────
|
||
function closeDetailTimers() {
|
||
if (detailState && detailState.totpInterval) {
|
||
clearInterval(detailState.totpInterval);
|
||
detailState.totpInterval = null;
|
||
}
|
||
}
|
||
|
||
function openDetail(id) {
|
||
const e = entryIndex[String(id)];
|
||
if (!e) return;
|
||
closeDetailTimers();
|
||
detailState = { id: String(id), password: null, revealUser: true, revealPass: false, totpInterval: null };
|
||
|
||
$('listWrap').style.display = 'none';
|
||
$('search').closest('.search-wrap').style.display = 'none';
|
||
$('newEntryPanel').style.display = 'none';
|
||
$('detailPanel').style.display = 'block';
|
||
|
||
$('detailHdTitle').textContent = e.title || 'Eintrag';
|
||
$('detailName').textContent = e.title || '';
|
||
|
||
// Icon: Favicon (gecacht) oder Monogramm
|
||
const icon = $('detailIcon');
|
||
const m = monogram(e.title);
|
||
icon.style.background = `hsl(${m.hue},52%,90%)`;
|
||
icon.innerHTML = `<span style="color:hsl(${m.hue},55%,38%);">${m.ch}</span>`;
|
||
if (e.favicon_domain) {
|
||
chrome.runtime.sendMessage({ type: 'GET_FAVICON', id }, resp => {
|
||
if (resp?.dataUrl && detailState && detailState.id === String(id)) {
|
||
icon.style.background = '#eef0f7';
|
||
icon.innerHTML = '<img src="' + resp.dataUrl + '" alt="">';
|
||
}
|
||
});
|
||
}
|
||
|
||
// URL
|
||
const urlLink = $('detailUrlLink');
|
||
const firstUrl = String(e.url || '').split('\n')[0].trim();
|
||
if (firstUrl) {
|
||
urlLink.textContent = firstUrl;
|
||
urlLink.href = /^https?:\/\//i.test(firstUrl) ? firstUrl : 'https://' + firstUrl;
|
||
urlLink.style.display = '';
|
||
} else {
|
||
urlLink.style.display = 'none';
|
||
}
|
||
|
||
// Benutzername (standardmäßig sichtbar; Auge schaltet Maskierung)
|
||
const uval = e.username || '';
|
||
const uEl = $('detailUser');
|
||
uEl.dataset.value = uval;
|
||
detailState.revealUser = true;
|
||
if (uval) {
|
||
uEl.classList.remove('empty');
|
||
uEl.textContent = uval;
|
||
$('btnRevealUser').style.display = '';
|
||
$('btnCopyUser').style.display = '';
|
||
} else {
|
||
uEl.classList.add('empty');
|
||
uEl.textContent = 'Kein Benutzername';
|
||
$('btnRevealUser').style.display = 'none';
|
||
$('btnCopyUser').style.display = 'none';
|
||
}
|
||
|
||
// Passwort (standardmäßig maskiert)
|
||
detailState.revealPass = false;
|
||
$('detailPass').textContent = '••••••••••';
|
||
|
||
// Notizen
|
||
const notes = (e.notes || '').trim();
|
||
if (notes) {
|
||
$('detailNotes').textContent = notes;
|
||
$('detailNotes').dataset.value = notes;
|
||
$('fieldNotes').style.display = '';
|
||
} else {
|
||
$('fieldNotes').style.display = 'none';
|
||
}
|
||
|
||
// TOTP
|
||
if (e.has_totp) {
|
||
$('fieldTotp').style.display = '';
|
||
loadDetailTotp(String(id));
|
||
} else {
|
||
$('fieldTotp').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function closeDetail() {
|
||
closeDetailTimers();
|
||
detailState = null;
|
||
$('detailPanel').style.display = 'none';
|
||
$('listWrap').style.display = '';
|
||
$('search').closest('.search-wrap').style.display = '';
|
||
}
|
||
|
||
function toggleRevealUser() {
|
||
const uEl = $('detailUser');
|
||
const val = uEl.dataset.value || '';
|
||
if (!val) return;
|
||
detailState.revealUser = !detailState.revealUser;
|
||
uEl.textContent = detailState.revealUser ? val : '•'.repeat(Math.min(val.length, 14));
|
||
}
|
||
|
||
async function ensurePassword(id) {
|
||
if (detailState && detailState.password !== null) return detailState.password;
|
||
const pw = await new Promise(resolve => {
|
||
chrome.runtime.sendMessage({ type: 'GET_PASSWORD', id }, resp => resolve(resp?.password ?? null));
|
||
});
|
||
if (detailState && detailState.id === String(id)) detailState.password = pw || '';
|
||
return pw || '';
|
||
}
|
||
|
||
async function toggleRevealPass() {
|
||
const pEl = $('detailPass');
|
||
if (detailState.revealPass) {
|
||
detailState.revealPass = false;
|
||
pEl.textContent = '••••••••••';
|
||
return;
|
||
}
|
||
pEl.textContent = '…';
|
||
const pw = await ensurePassword(detailState.id);
|
||
if (!detailState) return;
|
||
detailState.revealPass = true;
|
||
pEl.textContent = pw || '(leer)';
|
||
}
|
||
|
||
async function copyDetailPassword() {
|
||
const pw = await ensurePassword(detailState.id);
|
||
if (pw) copySecret(pw, 'Passwort kopiert');
|
||
else showToast('Kein Passwort');
|
||
}
|
||
|
||
function loadDetailTotp(id) {
|
||
const codeEl = $('detailTotp');
|
||
const secsEl = $('detailTotpSecs');
|
||
const barEl = $('detailTotpBar');
|
||
codeEl.textContent = '…';
|
||
codeEl.dataset.code = '';
|
||
secsEl.textContent = '';
|
||
|
||
chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, resp => {
|
||
if (!detailState || detailState.id !== String(id)) return;
|
||
if (!resp?.code) { codeEl.textContent = '—'; return; }
|
||
|
||
const apply = (code, remaining) => {
|
||
codeEl.textContent = code.slice(0, 3) + ' ' + code.slice(3);
|
||
codeEl.dataset.code = code;
|
||
secsEl.textContent = remaining + 's';
|
||
if (barEl) barEl.style.width = Math.round(remaining / 30 * 100) + '%';
|
||
};
|
||
apply(resp.code, resp.remaining);
|
||
|
||
let secs = resp.remaining;
|
||
detailState.totpInterval = setInterval(() => {
|
||
secs--;
|
||
if (secs <= 0) {
|
||
chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, r2 => {
|
||
if (!detailState || detailState.id !== String(id)) return;
|
||
if (r2?.code) { secs = r2.remaining; apply(r2.code, r2.remaining); }
|
||
});
|
||
return;
|
||
}
|
||
secsEl.textContent = secs + 's';
|
||
if (barEl) {
|
||
barEl.style.width = Math.round(secs / 30 * 100) + '%';
|
||
barEl.style.background = secs < 10 ? '#dc3545' : '#34d399';
|
||
}
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
async function fillActiveTab() {
|
||
if (!detailState) return;
|
||
const e = entryIndex[detailState.id];
|
||
if (!e) return;
|
||
const btn = $('btnDetailFill');
|
||
btn.disabled = true;
|
||
const pw = await ensurePassword(detailState.id);
|
||
|
||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||
const tab = tabs[0];
|
||
if (!tab || !tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
|
||
showToast('Auf dieser Seite nicht möglich');
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
// Sicherheit: Warnen, wenn die aktive Seite NICHT zur URL des Eintrags
|
||
// passt (verhindert versehentliches Ausfüllen auf einer fremden Domain).
|
||
if (!fillDomainMatches(e.url, tab.url)) {
|
||
const host = hostOf(tab.url);
|
||
if (!window.confirm('Diese Seite (' + host + ') passt nicht zur hinterlegten Adresse des Eintrags. Zugangsdaten trotzdem hier ausfüllen?')) {
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
}
|
||
chrome.tabs.sendMessage(tab.id, { type: 'VAULT_FILL', id: detailState.id, username: e.username || '', password: pw || '', has_totp: !!e.has_totp }, () => {
|
||
if (chrome.runtime.lastError) {
|
||
showToast('Seite nicht bereit – neu laden');
|
||
btn.disabled = false;
|
||
} else {
|
||
showToast('Ausgefüllt');
|
||
setTimeout(() => window.close(), 350);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Helfer ────────────────────────────────────────────────────────────────
|
||
function hostOf(u) {
|
||
try {
|
||
const s = String(u || '');
|
||
return new URL(s.includes('://') ? s : 'https://' + s).hostname.replace(/^www\./, '').toLowerCase();
|
||
} catch { return ''; }
|
||
}
|
||
// True, wenn eine der (mehrzeiligen) Eintrags-URLs zur Seiten-Domain passt –
|
||
// oder wenn im Eintrag gar keine URL hinterlegt ist (dann keine Warnung).
|
||
function fillDomainMatches(entryUrls, pageUrl) {
|
||
const pageHost = hostOf(pageUrl);
|
||
const list = String(entryUrls || '').split('\n').map(s => s.trim()).filter(Boolean);
|
||
if (!list.length) return true;
|
||
if (!pageHost) return false;
|
||
return list.some(u => {
|
||
let eh = hostOf(u);
|
||
if (eh.startsWith('*.')) eh = eh.slice(2);
|
||
return eh && (pageHost === eh || pageHost.endsWith('.' + eh) || eh.endsWith('.' + pageHost));
|
||
});
|
||
}
|
||
function copyToClipboard(text, msg) {
|
||
navigator.clipboard.writeText(text).then(() => showToast(msg)).catch(() => showToast('Fehler'));
|
||
}
|
||
// Wie copyToClipboard, plant aber zusätzlich das automatische Leeren der
|
||
// Zwischenablage (für Zugangsdaten/2FA).
|
||
function copySecret(text, msg) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
showToast(msg);
|
||
chrome.runtime.sendMessage({ type: 'SCHEDULE_CLIP_CLEAR', text });
|
||
}).catch(() => showToast('Fehler'));
|
||
}
|
||
function showToast(msg) {
|
||
const t = $('toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
setTimeout(() => t.classList.remove('show'), 1800);
|
||
}
|
||
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
function escAttr(s) { return String(s||'').replace(/"/g,'"'); }
|
||
|
||
init(); |