04/19 Update extension: persistent session, duplication check - again
This commit is contained in:
@@ -65,6 +65,13 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
|
||||||
|
// No-op ping from popup — keeps the service worker alive while the browser
|
||||||
|
// is open so chrome.storage.session is not wiped between popup openings.
|
||||||
|
if (msg.type === 'KEEPALIVE') {
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Popup signals vault cache was refreshed → re-check all tab badges.
|
// Popup signals vault cache was refreshed → re-check all tab badges.
|
||||||
if (msg.type === 'VAULT_UPDATED') {
|
if (msg.type === 'VAULT_UPDATED') {
|
||||||
refreshAllBadges();
|
refreshAllBadges();
|
||||||
|
|||||||
@@ -227,9 +227,60 @@
|
|||||||
injectAutofillButtons(matching);
|
injectAutofillButtons(matching);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Duplicate detection ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the cached vault to determine whether the submitted credentials are
|
||||||
|
* new or have been updated since last saved.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* 'new' — no item found for this site; definitely show the banner.
|
||||||
|
* 'updated' — item exists but username/password differs; show update banner.
|
||||||
|
* 'same' — credentials are identical to a stored item; suppress the banner.
|
||||||
|
*/
|
||||||
|
async function classifyCredentials(username, password) {
|
||||||
|
let vault_items;
|
||||||
|
try {
|
||||||
|
({ vault_items } = await chrome.storage.session.get('vault_items'));
|
||||||
|
} catch {
|
||||||
|
// If we can't read storage (e.g. extension context invalidated), show banner.
|
||||||
|
return 'new';
|
||||||
|
}
|
||||||
|
if (!vault_items?.length) return 'new';
|
||||||
|
|
||||||
|
const host = location.hostname.replace(/^www\./, '');
|
||||||
|
|
||||||
|
const siteItems = vault_items.filter(item => {
|
||||||
|
if (item.item_type !== 'password' || !item.plain?.url) return false;
|
||||||
|
try {
|
||||||
|
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
|
||||||
|
return h === host || h.endsWith(`.${host}`) || host.endsWith(`.${h}`);
|
||||||
|
} catch { return false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!siteItems.length) return 'new';
|
||||||
|
|
||||||
|
// Check for exact match (same username AND same password).
|
||||||
|
const exactMatch = siteItems.some(
|
||||||
|
item => item.plain?.username === username && item.plain?.password === password
|
||||||
|
);
|
||||||
|
if (exactMatch) return 'same';
|
||||||
|
|
||||||
|
// Credentials differ → treat as updated.
|
||||||
|
return 'updated';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Auto-save banner ──────────────────────────────────────────────────────────
|
// ── Auto-save banner ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function showSaveBanner(username, password) {
|
/**
|
||||||
|
* Shows the save/update banner. The banner stays visible until the user
|
||||||
|
* explicitly clicks "Save" or "Not now" — there is NO auto-dismiss timeout.
|
||||||
|
*
|
||||||
|
* @param {string} username
|
||||||
|
* @param {string} password
|
||||||
|
* @param {'new'|'updated'} credentialState — controls the title copy.
|
||||||
|
*/
|
||||||
|
function showSaveBanner(username, password, credentialState) {
|
||||||
if (_bannerEl) _bannerEl.remove();
|
if (_bannerEl) _bannerEl.remove();
|
||||||
|
|
||||||
const banner = document.createElement('div');
|
const banner = document.createElement('div');
|
||||||
@@ -251,13 +302,16 @@
|
|||||||
minWidth: '240px',
|
minWidth: '240px',
|
||||||
});
|
});
|
||||||
|
|
||||||
const site = escHtml(location.hostname);
|
const site = escHtml(location.hostname);
|
||||||
const user = escHtml(username);
|
const user = escHtml(username);
|
||||||
|
const title = credentialState === 'updated'
|
||||||
|
? 'Update in PassKeeper?'
|
||||||
|
: 'Save to PassKeeper?';
|
||||||
|
|
||||||
banner.innerHTML = `
|
banner.innerHTML = `
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="3" y="9" width="18" height="12" rx="2" stroke="#1a73e8" stroke-width="1.8"/><path d="M8 9V6a4 4 0 018 0v3" stroke="#1a73e8" stroke-width="1.8" stroke-linecap="round"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="3" y="9" width="18" height="12" rx="2" stroke="#1a73e8" stroke-width="1.8"/><path d="M8 9V6a4 4 0 018 0v3" stroke="#1a73e8" stroke-width="1.8" stroke-linecap="round"/></svg>
|
||||||
<strong style="flex:1;font-size:13px;color:#111827;">Save to PassKeeper?</strong>
|
<strong style="flex:1;font-size:13px;color:#111827;">${escHtml(title)}</strong>
|
||||||
<button id="__pk_close__" style="background:none;border:none;cursor:pointer;font-size:18px;color:#9ca3af;line-height:1;padding:0;">×</button>
|
<button id="__pk_close__" style="background:none;border:none;cursor:pointer;font-size:18px;color:#9ca3af;line-height:1;padding:0;">×</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="color:#6b7280;font-size:12px;margin-bottom:10px;">
|
<p style="color:#6b7280;font-size:12px;margin-bottom:10px;">
|
||||||
@@ -271,24 +325,24 @@
|
|||||||
document.body.appendChild(banner);
|
document.body.appendChild(banner);
|
||||||
_bannerEl = banner;
|
_bannerEl = banner;
|
||||||
|
|
||||||
|
// No setTimeout — banner stays until the user makes an explicit choice.
|
||||||
const dismiss = () => { if (_bannerEl === banner) { banner.remove(); _bannerEl = null; } };
|
const dismiss = () => { if (_bannerEl === banner) { banner.remove(); _bannerEl = null; } };
|
||||||
banner.querySelector('#__pk_close__').addEventListener('click', dismiss);
|
banner.querySelector('#__pk_close__').addEventListener('click', dismiss);
|
||||||
banner.querySelector('#__pk_skip__').addEventListener('click', dismiss);
|
banner.querySelector('#__pk_skip__').addEventListener('click', dismiss);
|
||||||
banner.querySelector('#__pk_save__').addEventListener('click', () => {
|
banner.querySelector('#__pk_save__').addEventListener('click', () => {
|
||||||
|
console.log('[PassKeeper] User chose to save credentials for', location.hostname);
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'SAVE_CREDENTIALS',
|
type: 'SAVE_CREDENTIALS',
|
||||||
data: { url: location.href, siteName: document.title || location.hostname, username, password },
|
data: { url: location.href, siteName: document.title || location.hostname, username, password },
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
dismiss();
|
dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(dismiss, 20_000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Form submission watch ─────────────────────────────────────────────────────
|
// ── Form submission watch ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function watchSubmissions() {
|
function watchSubmissions() {
|
||||||
document.addEventListener('submit', e => {
|
document.addEventListener('submit', async e => {
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
const pwField = form.querySelector('input[type="password"]:not([disabled])');
|
const pwField = form.querySelector('input[type="password"]:not([disabled])');
|
||||||
if (!pwField?.value) return;
|
if (!pwField?.value) return;
|
||||||
@@ -300,9 +354,18 @@
|
|||||||
const username = userField?.value?.trim() || '';
|
const username = userField?.value?.trim() || '';
|
||||||
const password = pwField.value;
|
const password = pwField.value;
|
||||||
|
|
||||||
if (username && password) {
|
if (!username || !password) return;
|
||||||
setTimeout(() => showSaveBanner(username, password), 500);
|
|
||||||
|
// Run duplicate check before showing the banner.
|
||||||
|
const credentialState = await classifyCredentials(username, password);
|
||||||
|
console.log('[PassKeeper] Credential state for', location.hostname, '→', credentialState);
|
||||||
|
|
||||||
|
if (credentialState === 'same') {
|
||||||
|
// Credentials unchanged — silently skip.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => showSaveBanner(username, password, credentialState), 500);
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -243,10 +243,10 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
padding: 8px 6px;
|
padding: 8px 4px;
|
||||||
background: none; border: none; cursor: pointer;
|
background: none; border: none; cursor: pointer;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
font-size: 10px; font-weight: 500;
|
font-size: 9px; font-weight: 500;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
.nav-btn:hover { color: #374151; }
|
.nav-btn:hover { color: #374151; }
|
||||||
|
|||||||
@@ -126,12 +126,28 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Vault</span>
|
<span>Vault</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" id="nav-account" title="Sign out">
|
<button class="nav-btn" id="nav-generator" title="Password Generator">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
|
||||||
|
<path d="M12 2a5 5 0 015 5v1h1a2 2 0 012 2v9a2 2 0 01-2 2H6a2 2 0 01-2-2v-9a2 2 0 012-2h1V7a5 5 0 015-5z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<circle cx="12" cy="14" r="1.8" fill="currentColor"/>
|
||||||
|
<path d="M12 14v2.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Generator</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" id="nav-alerts" title="Security Alerts">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
|
||||||
|
<path d="M12 3l8.5 15H3.5L12 3z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 10v4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span>Alerts</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" id="nav-account" title="Account">
|
||||||
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
|
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
|
||||||
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/>
|
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/>
|
||||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Sign out</span>
|
<span>Account</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
* we refresh the access_token and show the unlock-only view (master password only).
|
* we refresh the access_token and show the unlock-only view (master password only).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE = 'https://pwkeeper.ngodanguyen.tech';
|
const API_BASE = 'https://pwkeeper.ngodanguyen.tech';
|
||||||
const VAULT_URL = 'https://pwkeeper.ngodanguyen.tech/vault';
|
const VAULT_URL = 'https://pwkeeper.ngodanguyen.tech/vault';
|
||||||
|
const GEN_URL = 'https://pwkeeper.ngodanguyen.tech/vault#generator';
|
||||||
|
const ALERTS_URL = 'https://pwkeeper.ngodanguyen.tech/vault#security';
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -531,6 +533,15 @@ async function init() {
|
|||||||
// Vault actions
|
// Vault actions
|
||||||
$('btn-add-item').addEventListener('click', () => { chrome.tabs.create({ url: VAULT_URL }); });
|
$('btn-add-item').addEventListener('click', () => { chrome.tabs.create({ url: VAULT_URL }); });
|
||||||
$('nav-account').addEventListener('click', signOut);
|
$('nav-account').addEventListener('click', signOut);
|
||||||
|
$('nav-generator').addEventListener('click', () => { chrome.tabs.create({ url: GEN_URL }); });
|
||||||
|
$('nav-alerts').addEventListener('click', () => { chrome.tabs.create({ url: ALERTS_URL }); });
|
||||||
|
|
||||||
|
// Keep the service worker alive so chrome.storage.session survives while the
|
||||||
|
// browser is open. We ping it every 20 s; the ping itself is a no-op but
|
||||||
|
// prevents the SW from being killed between popup openings.
|
||||||
|
const _keepalive = setInterval(() => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'KEEPALIVE' }).catch(() => {});
|
||||||
|
}, 20_000);
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
$('vault-search').addEventListener('input', () => renderList());
|
$('vault-search').addEventListener('input', () => renderList());
|
||||||
|
|||||||
Reference in New Issue
Block a user