05/18 Enhanced codes and functionalities 12
This commit is contained in:
+76
-11
@@ -51,6 +51,45 @@ const Vault = (() => {
|
|||||||
document.addEventListener(e, handler, { passive: true }),
|
document.addEventListener(e, handler, { passive: true }),
|
||||||
);
|
);
|
||||||
_resetWebIdleTimer();
|
_resetWebIdleTimer();
|
||||||
|
|
||||||
|
// Lock when the tab is hidden for longer than the idle timeout.
|
||||||
|
// This catches screen-lock, minimize, and long tab switches without
|
||||||
|
// requiring the user to wait for the inactivity timer to fire after
|
||||||
|
// returning to the tab.
|
||||||
|
let _hiddenAt = null;
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "hidden") {
|
||||||
|
_hiddenAt = Date.now();
|
||||||
|
// Pause the inactivity timer while hidden — user activity is impossible.
|
||||||
|
if (_webIdleTimer) {
|
||||||
|
clearTimeout(_webIdleTimer);
|
||||||
|
_webIdleTimer = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tab became visible again.
|
||||||
|
const mins = _getWebIdleMinutes();
|
||||||
|
if (mins > 0 && _hiddenAt !== null) {
|
||||||
|
const hiddenMs = Date.now() - _hiddenAt;
|
||||||
|
if (hiddenMs >= mins * 60_000 && VaultSession.getKey()) {
|
||||||
|
console.log(
|
||||||
|
"[PassKeeper] Locking — tab was hidden for",
|
||||||
|
Math.round(hiddenMs / 1000),
|
||||||
|
"s (idle timeout:",
|
||||||
|
mins,
|
||||||
|
"min)",
|
||||||
|
);
|
||||||
|
VaultSession.clear();
|
||||||
|
showUnlockOverlay();
|
||||||
|
showToast("Vault locked due to inactivity.", "info");
|
||||||
|
_hiddenAt = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_hiddenAt = null;
|
||||||
|
// Resume the inactivity timer now that the user is back.
|
||||||
|
_resetWebIdleTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let _activeFilter = null;
|
let _activeFilter = null;
|
||||||
let _sortOrder = "name-asc";
|
let _sortOrder = "name-asc";
|
||||||
@@ -863,16 +902,18 @@ const Vault = (() => {
|
|||||||
|
|
||||||
const cutoff = Date.now() - 180 * 86400000;
|
const cutoff = Date.now() - 180 * 86400000;
|
||||||
// "Old" only penalises passwords that are ALSO weak or reused.
|
// "Old" only penalises passwords that are ALSO weak or reused.
|
||||||
// A strong, unique password that hasn't changed in 200 days is fine —
|
// Uses plain.password_changed_at when available (set on create/edit),
|
||||||
// penalising it discourages good password hygiene.
|
// falling back to created_at for items saved before this feature.
|
||||||
const weakOrReusedIds = new Set([
|
const weakOrReusedIds = new Set([
|
||||||
...weak.map((i) => i.id),
|
...weak.map((i) => i.id),
|
||||||
...reused.map((i) => i.id),
|
...reused.map((i) => i.id),
|
||||||
]);
|
]);
|
||||||
const old = pwItems.filter(
|
const old = pwItems.filter((i) => {
|
||||||
(i) =>
|
const ageRef = i.plain?.password_changed_at
|
||||||
new Date(i.created_at).getTime() < cutoff && weakOrReusedIds.has(i.id),
|
? new Date(i.plain.password_changed_at).getTime()
|
||||||
);
|
: new Date(i.created_at).getTime();
|
||||||
|
return ageRef < cutoff && weakOrReusedIds.has(i.id);
|
||||||
|
});
|
||||||
|
|
||||||
const total = pwItems.length;
|
const total = pwItems.length;
|
||||||
const score = Math.max(
|
const score = Math.max(
|
||||||
@@ -3495,10 +3536,34 @@ const Vault = (() => {
|
|||||||
mode === "add"
|
mode === "add"
|
||||||
? getVal("field-type")
|
? getVal("field-type")
|
||||||
: form.dataset.itemType || "password";
|
: form.dataset.itemType || "password";
|
||||||
const { enc_data, iv } = await Crypto.encryptItem(
|
|
||||||
vaultKey,
|
const plainData = buildPlainData(itemType);
|
||||||
buildPlainData(itemType),
|
|
||||||
);
|
// Track when the password was last changed — stored inside the encrypted
|
||||||
|
// blob so the server never sees it. Used by the security dashboard to
|
||||||
|
// accurately flag old passwords vs. old items that had their password changed.
|
||||||
|
if (itemType === "password") {
|
||||||
|
if (mode === "add") {
|
||||||
|
// New item — set password_changed_at to now.
|
||||||
|
plainData.password_changed_at = new Date().toISOString();
|
||||||
|
} else {
|
||||||
|
// Edit — only update if the password field actually changed.
|
||||||
|
const existingItem = _items.find((i) => i.id === parseInt(itemId));
|
||||||
|
const existingPassword = existingItem?.plain?.password ?? null;
|
||||||
|
const existingChangedAt = existingItem?.plain?.password_changed_at ?? null;
|
||||||
|
if (plainData.password !== existingPassword) {
|
||||||
|
// Password changed — record now.
|
||||||
|
plainData.password_changed_at = new Date().toISOString();
|
||||||
|
} else if (existingChangedAt) {
|
||||||
|
// Password unchanged — preserve the existing timestamp.
|
||||||
|
plainData.password_changed_at = existingChangedAt;
|
||||||
|
}
|
||||||
|
// If no existing timestamp and password unchanged, leave it absent
|
||||||
|
// — the security dashboard will fall back to created_at.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { enc_data, iv } = await Crypto.encryptItem(vaultKey, plainData);
|
||||||
// Encrypt the item name client-side so it is never stored in plaintext.
|
// Encrypt the item name client-side so it is never stored in plaintext.
|
||||||
const { enc_name, iv_name } = await Crypto.encryptName(vaultKey, name);
|
const { enc_name, iv_name } = await Crypto.encryptName(vaultKey, name);
|
||||||
const folderVal = getVal("field-folder");
|
const folderVal = getVal("field-folder");
|
||||||
@@ -4347,4 +4412,4 @@ const Vault = (() => {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||||
@@ -1273,6 +1273,7 @@ async function saveCredential(data) {
|
|||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
password: data.password || "",
|
password: data.password || "",
|
||||||
notes: "",
|
notes: "",
|
||||||
|
password_changed_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const name = $("save-name").value.trim() || data.siteName || "Untitled";
|
const name = $("save-name").value.trim() || data.siteName || "Untitled";
|
||||||
// Read selected folder — empty string means no folder (null).
|
// Read selected folder — empty string means no folder (null).
|
||||||
@@ -1577,7 +1578,7 @@ async function addItemToVault() {
|
|||||||
btn.textContent = "Saving…";
|
btn.textContent = "Saving…";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const plain = { url, username, password, notes };
|
const plain = { url, username, password, notes, password_changed_at: new Date().toISOString() };
|
||||||
const { enc_data, iv } = await ExtCrypto.encryptItem(_vaultKey, plain);
|
const { enc_data, iv } = await ExtCrypto.encryptItem(_vaultKey, plain);
|
||||||
const { enc_name, iv_name } = await ExtCrypto.encryptName(_vaultKey, name);
|
const { enc_name, iv_name } = await ExtCrypto.encryptName(_vaultKey, name);
|
||||||
const res = await apiFetch("/api/vault", {
|
const res = await apiFetch("/api/vault", {
|
||||||
|
|||||||
Reference in New Issue
Block a user