05/19 Enhance codes
This commit is contained in:
+29
-16
@@ -943,24 +943,22 @@ const Vault = (() => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
sectionsEl.innerHTML = "";
|
sectionsEl.innerHTML = "";
|
||||||
const makeSection = (title, icon, items, desc) => {
|
const makeSection = (title, icon, items, desc, renderItem) => {
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
const sec = document.createElement("div");
|
const sec = document.createElement("div");
|
||||||
sec.className = "sec-section";
|
sec.className = "sec-section";
|
||||||
|
const defaultRender = (i) => `<li class="sec-item">
|
||||||
|
<span class="sec-item-name">${escHtml(i.name)}</span>
|
||||||
|
<span class="sec-item-sub">${escHtml(i.plain?.username || "")}</span>
|
||||||
|
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
||||||
|
</li>`;
|
||||||
|
const renderFn = renderItem || defaultRender;
|
||||||
sec.innerHTML = `
|
sec.innerHTML = `
|
||||||
<div class="sec-section-header"><span class="sec-section-icon">${icon}</span>
|
<div class="sec-section-header"><span class="sec-section-icon">${icon}</span>
|
||||||
<div><div class="sec-section-title">${title} (${items.length})</div><div class="sec-section-desc">${desc}</div></div>
|
<div><div class="sec-section-title">${title} (${items.length})</div><div class="sec-section-desc">${desc}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sec-item-list">
|
<ul class="sec-item-list">
|
||||||
${items
|
${items.map(renderFn).join("")}
|
||||||
.map(
|
|
||||||
(i) => `<li class="sec-item">
|
|
||||||
<span class="sec-item-name">${escHtml(i.name)}</span>
|
|
||||||
<span class="sec-item-sub">${escHtml(i.plain?.username || "")}</span>
|
|
||||||
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
|
||||||
</li>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</ul>`;
|
</ul>`;
|
||||||
sec.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
sec.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
@@ -995,6 +993,20 @@ const Vault = (() => {
|
|||||||
"🕐",
|
"🕐",
|
||||||
old,
|
old,
|
||||||
"Weak or reused passwords not changed in over 180 days.",
|
"Weak or reused passwords not changed in over 180 days.",
|
||||||
|
(i) => {
|
||||||
|
const ref = i.plain?.password_changed_at || i.created_at;
|
||||||
|
const daysAgo = ref
|
||||||
|
? Math.floor((Date.now() - new Date(ref).getTime()) / 86400000)
|
||||||
|
: null;
|
||||||
|
const ageLabel = daysAgo !== null
|
||||||
|
? `Last changed ${daysAgo} day${daysAgo !== 1 ? "s" : ""} ago`
|
||||||
|
: "Age unknown";
|
||||||
|
return `<li class="sec-item">
|
||||||
|
<span class="sec-item-name">${escHtml(i.name)}</span>
|
||||||
|
<span class="sec-item-sub">${escHtml(ageLabel)}</span>
|
||||||
|
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
||||||
|
</li>`;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Missing 2FA warning ──────────────────────────────────────────────────
|
// ── Missing 2FA warning ──────────────────────────────────────────────────
|
||||||
@@ -1423,6 +1435,9 @@ const Vault = (() => {
|
|||||||
payload = _importRows;
|
payload = _importRows;
|
||||||
} else {
|
} else {
|
||||||
// Plaintext CSV rows — encrypt each one now.
|
// Plaintext CSV rows — encrypt each one now.
|
||||||
|
// Use the import timestamp as password_changed_at — best available
|
||||||
|
// approximation since CSV exports don't carry a change date.
|
||||||
|
const importedAt = new Date().toISOString();
|
||||||
payload = await Promise.all(
|
payload = await Promise.all(
|
||||||
_importRows.map(async (row) => {
|
_importRows.map(async (row) => {
|
||||||
const plain = {
|
const plain = {
|
||||||
@@ -1430,6 +1445,7 @@ const Vault = (() => {
|
|||||||
username: row.username || "",
|
username: row.username || "",
|
||||||
password: row.password,
|
password: row.password,
|
||||||
notes: row.notes || "",
|
notes: row.notes || "",
|
||||||
|
password_changed_at: importedAt,
|
||||||
};
|
};
|
||||||
const { enc_data, iv } = await Crypto.encryptItem(
|
const { enc_data, iv } = await Crypto.encryptItem(
|
||||||
vaultKey,
|
vaultKey,
|
||||||
@@ -1607,8 +1623,7 @@ const Vault = (() => {
|
|||||||
<span class="share-name">${escHtml(s.item_name)}</span>
|
<span class="share-name">${escHtml(s.item_name)}</span>
|
||||||
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
|
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
|
||||||
</div>
|
</div>
|
||||||
${
|
${!s.accepted
|
||||||
!s.accepted
|
|
||||||
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
||||||
: `<button class="btn-secondary btn-sm"
|
: `<button class="btn-secondary btn-sm"
|
||||||
data-view-share="${s.id}"
|
data-view-share="${s.id}"
|
||||||
@@ -2155,8 +2170,7 @@ const Vault = (() => {
|
|||||||
<span class="em-vault-chevron">▸</span>
|
<span class="em-vault-chevron">▸</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
|
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
|
||||||
${
|
${hasFields
|
||||||
hasFields
|
|
||||||
? '<div class="detail-body em-detail-body"></div>'
|
? '<div class="detail-body em-detail-body"></div>'
|
||||||
: '<p class="vault-empty">Could not decrypt this item.</p>'
|
: '<p class="vault-empty">Could not decrypt this item.</p>'
|
||||||
}
|
}
|
||||||
@@ -2751,8 +2765,7 @@ const Vault = (() => {
|
|||||||
<h3 style="margin:0 0 8px;font-size:16px;color:#111827;">
|
<h3 style="margin:0 0 8px;font-size:16px;color:#111827;">
|
||||||
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
|
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
|
||||||
</h3>
|
</h3>
|
||||||
${
|
${isFirstTime
|
||||||
isFirstTime
|
|
||||||
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
||||||
These codes let you sign in if you lose access to your authenticator app.
|
These codes let you sign in if you lose access to your authenticator app.
|
||||||
<strong>Save them now — they will not be shown again.</strong>
|
<strong>Save them now — they will not be shown again.</strong>
|
||||||
|
|||||||
@@ -932,6 +932,7 @@ body {
|
|||||||
background: #e0f2fe;
|
background: #e0f2fe;
|
||||||
color: #0369a1;
|
color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
|
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
.pk-flyout {
|
.pk-flyout {
|
||||||
@@ -972,9 +973,20 @@ body {
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-flyout-info {
|
||||||
|
padding: 6px 12px 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
margin-top: 2px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Collapsible folder groups ────────────────────────────────────────────── */
|
/* ── Collapsible folder groups ────────────────────────────────────────────── */
|
||||||
|
|
||||||
.pk-group { margin-bottom: 2px; }
|
.pk-group {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-group-header {
|
.pk-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -988,7 +1000,9 @@ body {
|
|||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-group-header:hover { background: #f0f4ff; }
|
.pk-group-header:hover {
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-group-name {
|
.pk-group-name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1131,6 +1131,26 @@ function renderList() {
|
|||||||
: null,
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Add a non-action info row showing when the password was last changed.
|
||||||
|
// Only shown for password items that have password_changed_at set.
|
||||||
|
if (item.item_type === "password" && item.plain?.password_changed_at) {
|
||||||
|
const daysAgo = Math.floor(
|
||||||
|
(Date.now() - new Date(item.plain.password_changed_at).getTime()) /
|
||||||
|
86400000,
|
||||||
|
);
|
||||||
|
const ageText =
|
||||||
|
daysAgo === 0
|
||||||
|
? "Changed today"
|
||||||
|
: daysAgo === 1
|
||||||
|
? "Changed yesterday"
|
||||||
|
: `Changed ${daysAgo} days ago`;
|
||||||
|
const infoRow = document.createElement("div");
|
||||||
|
infoRow.className = "pk-flyout-info";
|
||||||
|
infoRow.textContent = ageText;
|
||||||
|
// Append after action rows are added.
|
||||||
|
flyout._ageInfoRow = infoRow;
|
||||||
|
}
|
||||||
|
|
||||||
menuItems.forEach((mi) => {
|
menuItems.forEach((mi) => {
|
||||||
const row = document.createElement("button");
|
const row = document.createElement("button");
|
||||||
row.className = "pk-flyout-item";
|
row.className = "pk-flyout-item";
|
||||||
@@ -1142,6 +1162,11 @@ function renderList() {
|
|||||||
flyout.appendChild(row);
|
flyout.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Append age info row if present.
|
||||||
|
if (flyout._ageInfoRow) {
|
||||||
|
flyout.appendChild(flyout._ageInfoRow);
|
||||||
|
}
|
||||||
|
|
||||||
// Position flyout using fixed coords so it escapes vault-list overflow clipping.
|
// Position flyout using fixed coords so it escapes vault-list overflow clipping.
|
||||||
const appEl = document.getElementById("app");
|
const appEl = document.getElementById("app");
|
||||||
appEl.appendChild(flyout);
|
appEl.appendChild(flyout);
|
||||||
|
|||||||
Reference in New Issue
Block a user