05/07: updated extension after updating Advanced Settings to functioning
This commit is contained in:
@@ -167,7 +167,7 @@
|
|||||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function doAutofill(username, password) {
|
function doAutofill(username, password, autologin) {
|
||||||
const pwFields = visiblePasswordFields();
|
const pwFields = visiblePasswordFields();
|
||||||
if (!pwFields.length) return;
|
if (!pwFields.length) return;
|
||||||
const pwField = pwFields[0];
|
const pwField = pwFields[0];
|
||||||
@@ -180,6 +180,24 @@
|
|||||||
el.style.outline = "";
|
el.style.outline = "";
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
// Autologin: submit the form automatically after filling.
|
||||||
|
if (autologin) {
|
||||||
|
const form = pwField.closest("form");
|
||||||
|
if (form) {
|
||||||
|
setTimeout(function () {
|
||||||
|
// Prefer clicking a visible submit button so site-specific submit
|
||||||
|
// handlers (React, Vue, etc.) fire correctly.
|
||||||
|
var submitBtn = form.querySelector(
|
||||||
|
'[type="submit"]:not([disabled])',
|
||||||
|
);
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.click();
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Icon button (fixed-position, outside the DOM tree of the field) ───────────
|
// ── Icon button (fixed-position, outside the DOM tree of the field) ───────────
|
||||||
@@ -1105,7 +1123,7 @@
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {
|
chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {
|
||||||
if (msg.type === "DO_AUTOFILL") {
|
if (msg.type === "DO_AUTOFILL") {
|
||||||
doAutofill(msg.username, msg.password);
|
doAutofill(msg.username, msg.password, !!msg.autologin);
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
}
|
}
|
||||||
if (msg.type === "VAULT_UPDATED") {
|
if (msg.type === "VAULT_UPDATED") {
|
||||||
|
|||||||
@@ -1045,3 +1045,52 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Master-password reprompt overlay ────────────────────────────── */
|
||||||
|
.reprompt-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 99999;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reprompt-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 290px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reprompt-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a2e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reprompt-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reprompt-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reprompt-actions .btn-primary,
|
||||||
|
.reprompt-actions .btn-ghost {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|||||||
+107
-12
@@ -77,6 +77,81 @@ function _copyWithAutoClear(text) {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an in-popup master-password re-prompt overlay.
|
||||||
|
* Resolves true if the entered password matches the current vault key,
|
||||||
|
* false if the user cancels or enters a wrong password.
|
||||||
|
*/
|
||||||
|
async function _repromptMasterPassword() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Build the overlay element.
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.id = "pk-reprompt-overlay";
|
||||||
|
overlay.className = "reprompt-overlay";
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="reprompt-card">
|
||||||
|
<div class="reprompt-header">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="3" y="10" width="18" height="12" rx="2" stroke="#c0392b" stroke-width="1.8"/>
|
||||||
|
<path d="M8 10V7a4 4 0 018 0v3" stroke="#c0392b" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Master password required</span>
|
||||||
|
</div>
|
||||||
|
<p class="reprompt-desc">This item is protected. Enter your master password to continue.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reprompt-pw">Master Password</label>
|
||||||
|
<input type="password" id="reprompt-pw" placeholder="Master password" autocomplete="current-password"/>
|
||||||
|
</div>
|
||||||
|
<p id="reprompt-error" class="pk-error hidden">Incorrect password.</p>
|
||||||
|
<div class="reprompt-actions">
|
||||||
|
<button id="reprompt-confirm" class="btn-primary btn-sm">Confirm</button>
|
||||||
|
<button id="reprompt-cancel" class="btn-ghost btn-sm">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.getElementById("app").appendChild(overlay);
|
||||||
|
const input = overlay.querySelector("#reprompt-pw");
|
||||||
|
const errEl = overlay.querySelector("#reprompt-error");
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
async function attempt() {
|
||||||
|
errEl.classList.add("hidden");
|
||||||
|
const pw = input.value;
|
||||||
|
if (!pw) { errEl.textContent = "Enter your master password."; errEl.classList.remove("hidden"); return; }
|
||||||
|
try {
|
||||||
|
let { enc_key_salt } = await chrome.storage.session.get("enc_key_salt");
|
||||||
|
if (!enc_key_salt) {
|
||||||
|
const local = await chrome.storage.local.get("enc_key_salt");
|
||||||
|
enc_key_salt = local.enc_key_salt;
|
||||||
|
}
|
||||||
|
if (!enc_key_salt) { errEl.textContent = "Session expired. Please log in again."; errEl.classList.remove("hidden"); return; }
|
||||||
|
// Derive a candidate key and compare its JWK to the stored key.
|
||||||
|
const candidate = await ExtCrypto.deriveVaultKey(pw, enc_key_salt);
|
||||||
|
const candidateJwk = await ExtCrypto.exportVaultKey(candidate);
|
||||||
|
const { vault_key_jwk } = await chrome.storage.session.get("vault_key_jwk");
|
||||||
|
if (!vault_key_jwk || JSON.stringify(candidateJwk) !== JSON.stringify(vault_key_jwk)) {
|
||||||
|
throw new Error("mismatch");
|
||||||
|
}
|
||||||
|
cleanup(true);
|
||||||
|
} catch {
|
||||||
|
errEl.textContent = "Incorrect password.";
|
||||||
|
errEl.classList.remove("hidden");
|
||||||
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(result) {
|
||||||
|
overlay.remove();
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.querySelector("#reprompt-confirm").addEventListener("click", attempt);
|
||||||
|
overlay.querySelector("#reprompt-cancel").addEventListener("click", () => cleanup(false));
|
||||||
|
input.addEventListener("keydown", (e) => { if (e.key === "Enter") attempt(); if (e.key === "Escape") cleanup(false); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Avatar helpers ────────────────────────────────────────────────────────────
|
// ── Avatar helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const AVATAR_COLORS = [
|
const AVATAR_COLORS = [
|
||||||
@@ -694,7 +769,8 @@ function renderList() {
|
|||||||
const canFill =
|
const canFill =
|
||||||
item.item_type === "password" &&
|
item.item_type === "password" &&
|
||||||
item.plain?.username &&
|
item.plain?.username &&
|
||||||
item.plain?.password;
|
item.plain?.password &&
|
||||||
|
item.plain?.autofill !== false; // respect Advanced Setting
|
||||||
const canCopy = item.item_type === "password" && item.plain?.password;
|
const canCopy = item.item_type === "password" && item.plain?.password;
|
||||||
const hasTotp =
|
const hasTotp =
|
||||||
item.item_type === "password" &&
|
item.item_type === "password" &&
|
||||||
@@ -799,16 +875,19 @@ function renderList() {
|
|||||||
|
|
||||||
// Copy password
|
// Copy password
|
||||||
listEl.querySelectorAll("[data-copy-pass]").forEach((btn) =>
|
listEl.querySelectorAll("[data-copy-pass]").forEach((btn) =>
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyPass));
|
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyPass));
|
||||||
if (item?.plain?.password) {
|
if (!item?.plain?.password) return;
|
||||||
|
if (item.plain?.reprompt) {
|
||||||
|
const ok = await _repromptMasterPassword();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
_copyWithAutoClear(item.plain.password);
|
_copyWithAutoClear(item.plain.password);
|
||||||
btn.title = "Copied!";
|
btn.title = "Copied!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.title = "Copy password";
|
btn.title = "Copy password";
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -837,6 +916,10 @@ function renderList() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.autofill));
|
const item = _items.find((i) => i.id === parseInt(btn.dataset.autofill));
|
||||||
if (!item?.plain) return;
|
if (!item?.plain) return;
|
||||||
|
if (item.plain?.reprompt) {
|
||||||
|
const ok = await _repromptMasterPassword();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
const [tab] = await chrome.tabs.query({
|
const [tab] = await chrome.tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
@@ -847,6 +930,7 @@ function renderList() {
|
|||||||
type: "DO_AUTOFILL",
|
type: "DO_AUTOFILL",
|
||||||
username: item.plain.username || "",
|
username: item.plain.username || "",
|
||||||
password: item.plain.password || "",
|
password: item.plain.password || "",
|
||||||
|
autologin: !!item.plain.autologin,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -856,16 +940,19 @@ function renderList() {
|
|||||||
|
|
||||||
// Copy username (dedicated button)
|
// Copy username (dedicated button)
|
||||||
listEl.querySelectorAll("[data-copy-user]").forEach((btn) =>
|
listEl.querySelectorAll("[data-copy-user]").forEach((btn) =>
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyUser));
|
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyUser));
|
||||||
if (item?.plain?.username) {
|
if (!item?.plain?.username) return;
|
||||||
|
if (item.plain?.reprompt) {
|
||||||
|
const ok = await _repromptMasterPassword();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
_copyWithAutoClear(item.plain.username);
|
_copyWithAutoClear(item.plain.username);
|
||||||
btn.title = "Copied!";
|
btn.title = "Copied!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.title = "Copy username";
|
btn.title = "Copy username";
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -887,7 +974,7 @@ function renderList() {
|
|||||||
? {
|
? {
|
||||||
label: "Open URL",
|
label: "Open URL",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||||
action: () => {
|
action: async () => {
|
||||||
chrome.tabs.create({ url: item.plain.url });
|
chrome.tabs.create({ url: item.plain.url });
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
@@ -897,7 +984,11 @@ function renderList() {
|
|||||||
? {
|
? {
|
||||||
label: "Copy username",
|
label: "Copy username",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><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"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><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"/></svg>',
|
||||||
action: () => {
|
action: async () => {
|
||||||
|
if (item.plain?.reprompt) {
|
||||||
|
const ok = await _repromptMasterPassword();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
_copyWithAutoClear(item.plain.username);
|
_copyWithAutoClear(item.plain.username);
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
@@ -907,7 +998,11 @@ function renderList() {
|
|||||||
? {
|
? {
|
||||||
label: "Copy password",
|
label: "Copy password",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||||
action: () => {
|
action: async () => {
|
||||||
|
if (item.plain?.reprompt) {
|
||||||
|
const ok = await _repromptMasterPassword();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
_copyWithAutoClear(item.plain.password);
|
_copyWithAutoClear(item.plain.password);
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
@@ -919,9 +1014,9 @@ function renderList() {
|
|||||||
const row = document.createElement("button");
|
const row = document.createElement("button");
|
||||||
row.className = "pk-flyout-item";
|
row.className = "pk-flyout-item";
|
||||||
row.innerHTML = mi.icon + "<span>" + escHtml(mi.label) + "</span>";
|
row.innerHTML = mi.icon + "<span>" + escHtml(mi.label) + "</span>";
|
||||||
row.addEventListener("click", (e) => {
|
row.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
mi.action();
|
await mi.action();
|
||||||
});
|
});
|
||||||
flyout.appendChild(row);
|
flyout.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user