-
@@ -2120,7 +2123,7 @@ const Vault = (() => {
if (
(mode === "add" &&
(document.getElementById("field-type").value || "password") ===
- "password") ||
+ "password") ||
(mode === "edit" && (item?.item_type || "password") === "password")
) {
initPasswordFieldEnhancements();
@@ -2274,7 +2277,7 @@ const Vault = (() => {
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify({ refresh_token: refreshToken }),
- }).catch(() => { });
+ }).catch(() => {});
VaultSession.clear();
SharingSession.clear();
sessionStorage.removeItem("access_token");
@@ -2298,7 +2301,9 @@ const Vault = (() => {
try {
const secret = new URL(uri).searchParams.get("secret");
return secret ? secret.toUpperCase().replace(/\s+/g, "") : null;
- } catch (e) { return null; }
+ } catch (e) {
+ return null;
+ }
}
// Plain base32 secret (spaces and hyphens stripped for convenience).
return uri.toUpperCase().replace(/[\s-]/g, "") || null;
@@ -2308,14 +2313,18 @@ const Vault = (() => {
function base32ToBytes(b32) {
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
b32 = b32.replace(/=+$/, "");
- let bits = 0, val = 0;
+ let bits = 0,
+ val = 0;
const out = [];
for (const c of b32) {
const idx = CHARS.indexOf(c);
if (idx === -1) continue;
val = (val << 5) | idx;
bits += 5;
- if (bits >= 8) { bits -= 8; out.push((val >> bits) & 0xff); }
+ if (bits >= 8) {
+ bits -= 8;
+ out.push((val >> bits) & 0xff);
+ }
}
return new Uint8Array(out);
}
@@ -2332,15 +2341,28 @@ const Vault = (() => {
const msg = new Uint8Array(8);
// Write counter as big-endian 64-bit integer.
let c = counter;
- for (let i = 7; i >= 0; i--) { msg[i] = c & 0xff; c = Math.floor(c / 256); }
+ for (let i = 7; i >= 0; i--) {
+ msg[i] = c & 0xff;
+ c = Math.floor(c / 256);
+ }
const cryptoKey = await crypto.subtle.importKey(
- "raw", keyBytes, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]
+ "raw",
+ keyBytes,
+ { name: "HMAC", hash: "SHA-1" },
+ false,
+ ["sign"],
+ );
+ const sig = new Uint8Array(
+ await crypto.subtle.sign("HMAC", cryptoKey, msg),
);
- const sig = new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, msg));
const offset = sig[19] & 0x0f;
- const code = ((sig[offset] & 0x7f) << 24 | sig[offset + 1] << 16 |
- sig[offset + 2] << 8 | sig[offset + 3]) % 1_000_000;
+ const code =
+ (((sig[offset] & 0x7f) << 24) |
+ (sig[offset + 1] << 16) |
+ (sig[offset + 2] << 8) |
+ sig[offset + 3]) %
+ 1_000_000;
return String(code).padStart(6, "0");
}
@@ -2349,488 +2371,488 @@ const Vault = (() => {
return 30 - (Math.floor(Date.now() / 1000) % 30);
}
-
- navigator.clipboard
- .writeText(text)
- .then(() => showToast(msg))
- .catch(() => { });
-}
+ function copyToClipboard(text, msg) {
+ navigator.clipboard
+ .writeText(text)
+ .then(() => showToast(msg))
+ .catch(() => {});
+ }
function escHtml(str) {
- return String(str)
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
-}
-
-function itemIcon(type) {
- return (
- {
- password: "🔑",
- note: "📝",
- card: "💳",
- bank: "🏦",
- address: "🏠",
- ssn: "🪪",
- }[type] || "🔑"
- );
-}
-
-function showToast(message, type = "success") {
- const toast = document.getElementById("toast");
- if (!toast) return;
- toast.textContent = message;
- toast.className = `toast toast-${type} show`;
- clearTimeout(toast._timer);
- toast._timer = setTimeout(() => toast.classList.remove("show"), 2500);
-}
-
-function showLoadingState(loading) {
- document
- .getElementById("vault-spinner")
- ?.classList.toggle("hidden", !loading);
-}
-
-// ── Unlock overlay ────────────────────────────────────────────────────────
-
-function showUnlockOverlay() {
- document.getElementById("unlock-overlay").classList.add("open");
- document.getElementById("unlock-password").focus();
-}
-
-async function handleUnlock(e) {
- e.preventDefault();
- const password = document.getElementById("unlock-password").value;
- const enc_key_salt = sessionStorage.getItem("enc_key_salt");
- const btn = e.target.querySelector('[type="submit"]');
- const errEl = document.getElementById("unlock-error");
- if (!enc_key_salt) {
- redirectToLogin();
- return;
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
}
- btn.disabled = true;
- btn.textContent = "Unlocking…";
- errEl.classList.add("hidden");
- try {
- const vaultKey = await Crypto.deriveVaultKey(password, enc_key_salt);
- VaultSession.setKey(vaultKey);
- document.getElementById("unlock-overlay").classList.remove("open");
- await loadFolders();
- await loadVault();
- } catch {
- errEl.classList.remove("hidden");
- } finally {
- btn.disabled = false;
- btn.textContent = "Unlock";
- document.getElementById("unlock-password").value = "";
+ function itemIcon(type) {
+ return (
+ {
+ password: "🔑",
+ note: "📝",
+ card: "💳",
+ bank: "🏦",
+ address: "🏠",
+ ssn: "🪪",
+ }[type] || "🔑"
+ );
}
-}
-// ── Tabs ──────────────────────────────────────────────────────────────────
+ function showToast(message, type = "success") {
+ const toast = document.getElementById("toast");
+ if (!toast) return;
+ toast.textContent = message;
+ toast.className = `toast toast-${type} show`;
+ clearTimeout(toast._timer);
+ toast._timer = setTimeout(() => toast.classList.remove("show"), 2500);
+ }
-function initTabs(container) {
- const tabs = container.querySelectorAll(".tab-btn");
- tabs.forEach((btn) => {
- btn.addEventListener("click", () => {
- tabs.forEach((t) => t.classList.remove("active"));
- btn.classList.add("active");
- const target = btn.dataset.tab;
- container.querySelectorAll(".tab-pane").forEach((pane) => {
- pane.classList.toggle("hidden", !pane.id.endsWith(target));
- });
- });
- });
-}
+ function showLoadingState(loading) {
+ document
+ .getElementById("vault-spinner")
+ ?.classList.toggle("hidden", !loading);
+ }
-// ── Init ──────────────────────────────────────────────────────────────────
+ // ── Unlock overlay ────────────────────────────────────────────────────────
-async function init() {
- if (!sessionStorage.getItem("access_token")) {
- // Give the extension bridge up to 800 ms to inject a session before redirecting
- await new Promise((resolve) => {
- const timer = setTimeout(resolve, 800);
- window.addEventListener(
- "passkeeper:ext-login",
- () => {
- clearTimeout(timer);
- resolve();
- },
- { once: true },
- );
- });
+ function showUnlockOverlay() {
+ document.getElementById("unlock-overlay").classList.add("open");
+ document.getElementById("unlock-password").focus();
+ }
- // Still no token — try a silent refresh using the persisted refresh_token
- // before giving up and redirecting. This handles hard-refresh / new-tab scenarios.
- if (!sessionStorage.getItem("access_token")) {
- const refreshed = await tryRefreshToken();
- if (!refreshed) {
- window.location.href = "/login";
- return;
- }
+ async function handleUnlock(e) {
+ e.preventDefault();
+ const password = document.getElementById("unlock-password").value;
+ const enc_key_salt = sessionStorage.getItem("enc_key_salt");
+ const btn = e.target.querySelector('[type="submit"]');
+ const errEl = document.getElementById("unlock-error");
+ if (!enc_key_salt) {
+ redirectToLogin();
+ return;
+ }
+
+ btn.disabled = true;
+ btn.textContent = "Unlocking…";
+ errEl.classList.add("hidden");
+ try {
+ const vaultKey = await Crypto.deriveVaultKey(password, enc_key_salt);
+ VaultSession.setKey(vaultKey);
+ document.getElementById("unlock-overlay").classList.remove("open");
+ await loadFolders();
+ await loadVault();
+ } catch {
+ errEl.classList.remove("hidden");
+ } finally {
+ btn.disabled = false;
+ btn.textContent = "Unlock";
+ document.getElementById("unlock-password").value = "";
}
}
- // Vault
- document
- .getElementById("unlock-form")
- ?.addEventListener("submit", handleUnlock);
- document
- .getElementById("btn-new-folder")
- ?.addEventListener("click", showNewFolderRow);
- document
- .getElementById("btn-cancel-folder")
- ?.addEventListener("click", hideNewFolderRow);
- document
- .getElementById("new-folder-form")
- ?.addEventListener("submit", handleNewFolder);
- document
- .getElementById("search-input")
- ?.addEventListener("input", handleSearch);
- document
- .getElementById("btn-add-item")
- ?.addEventListener("click", () => openModal("add"));
- document
- .getElementById("btn-close-modal")
- ?.addEventListener("click", () => closeModal());
- document
- .getElementById("btn-cancel-modal")
- ?.addEventListener("click", () => closeModal());
- document
- .getElementById("item-form")
- ?.addEventListener("submit", handleFormSubmit);
- document
- .getElementById("btn-logout")
- ?.addEventListener("click", handleLogout);
+ // ── Tabs ──────────────────────────────────────────────────────────────────
- document.getElementById("sort-select")?.addEventListener("change", (e) => {
- _sortOrder = e.target.value;
- applyCurrentFilter();
- });
- document.getElementById("field-type")?.addEventListener("change", (e) => {
- switchModalType(e.target.value);
- if (e.target.value === "password") initPasswordFieldEnhancements();
- });
- document
- .getElementById("btn-toggle-pass")
- ?.addEventListener("click", () => {
- const el = document.getElementById("field-password");
+ function initTabs(container) {
+ const tabs = container.querySelectorAll(".tab-btn");
+ tabs.forEach((btn) => {
+ btn.addEventListener("click", () => {
+ tabs.forEach((t) => t.classList.remove("active"));
+ btn.classList.add("active");
+ const target = btn.dataset.tab;
+ container.querySelectorAll(".tab-pane").forEach((pane) => {
+ pane.classList.toggle("hidden", !pane.id.endsWith(target));
+ });
+ });
+ });
+ }
+
+ // ── Init ──────────────────────────────────────────────────────────────────
+
+ async function init() {
+ if (!sessionStorage.getItem("access_token")) {
+ // Give the extension bridge up to 800 ms to inject a session before redirecting
+ await new Promise((resolve) => {
+ const timer = setTimeout(resolve, 800);
+ window.addEventListener(
+ "passkeeper:ext-login",
+ () => {
+ clearTimeout(timer);
+ resolve();
+ },
+ { once: true },
+ );
+ });
+
+ // Still no token — try a silent refresh using the persisted refresh_token
+ // before giving up and redirecting. This handles hard-refresh / new-tab scenarios.
+ if (!sessionStorage.getItem("access_token")) {
+ const refreshed = await tryRefreshToken();
+ if (!refreshed) {
+ window.location.href = "/login";
+ return;
+ }
+ }
+ }
+
+ // Vault
+ document
+ .getElementById("unlock-form")
+ ?.addEventListener("submit", handleUnlock);
+ document
+ .getElementById("btn-new-folder")
+ ?.addEventListener("click", showNewFolderRow);
+ document
+ .getElementById("btn-cancel-folder")
+ ?.addEventListener("click", hideNewFolderRow);
+ document
+ .getElementById("new-folder-form")
+ ?.addEventListener("submit", handleNewFolder);
+ document
+ .getElementById("search-input")
+ ?.addEventListener("input", handleSearch);
+ document
+ .getElementById("btn-add-item")
+ ?.addEventListener("click", () => openModal("add"));
+ document
+ .getElementById("btn-close-modal")
+ ?.addEventListener("click", () => closeModal());
+ document
+ .getElementById("btn-cancel-modal")
+ ?.addEventListener("click", () => closeModal());
+ document
+ .getElementById("item-form")
+ ?.addEventListener("submit", handleFormSubmit);
+ document
+ .getElementById("btn-logout")
+ ?.addEventListener("click", handleLogout);
+
+ document.getElementById("sort-select")?.addEventListener("change", (e) => {
+ _sortOrder = e.target.value;
+ applyCurrentFilter();
+ });
+ document.getElementById("field-type")?.addEventListener("change", (e) => {
+ switchModalType(e.target.value);
+ if (e.target.value === "password") initPasswordFieldEnhancements();
+ });
+ document
+ .getElementById("btn-toggle-pass")
+ ?.addEventListener("click", () => {
+ const el = document.getElementById("field-password");
+ el.type = el.type === "password" ? "text" : "password";
+ });
+ document.getElementById("btn-toggle-ssn")?.addEventListener("click", () => {
+ const el = document.getElementById("field-ssn-number");
el.type = el.type === "password" ? "text" : "password";
});
- document.getElementById("btn-toggle-ssn")?.addEventListener("click", () => {
- const el = document.getElementById("field-ssn-number");
- el.type = el.type === "password" ? "text" : "password";
- });
- // Sidebar navigation
- document.getElementById("sidebar-all")?.addEventListener("click", () => {
- _activeFilter = null;
- switchView("vault");
- document
- .querySelectorAll(".sidebar-item")
- .forEach((el) => el.classList.remove("active"));
- document.getElementById("sidebar-all").classList.add("active");
- updateVaultTitle("All Items");
- renderItemList(_items);
- });
- document
- .getElementById("sidebar-security")
- ?.addEventListener("click", () => switchView("security"));
- document
- .getElementById("sidebar-sharing")
- ?.addEventListener("click", () => switchView("sharing"));
- document
- .getElementById("sidebar-emergency")
- ?.addEventListener("click", () => switchView("emergency"));
- document
- .getElementById("sidebar-generator")
- ?.addEventListener("click", () => openPasswordGeneratorModal());
-
- document.querySelectorAll("[data-type-filter]").forEach((el) => {
- el.addEventListener("click", () => {
- const type = el.dataset.typeFilter;
- const labels = {
- password: "Passwords",
- note: "Secure Notes",
- card: "Payment Cards",
- bank: "Bank Accounts",
- address: "Addresses",
- ssn: "Identities",
- };
- _activeFilter = { type: "itemType", value: type };
+ // Sidebar navigation
+ document.getElementById("sidebar-all")?.addEventListener("click", () => {
+ _activeFilter = null;
switchView("vault");
document
.querySelectorAll(".sidebar-item")
- .forEach((s) => s.classList.remove("active"));
- el.classList.add("active");
- updateVaultTitle(labels[type] || type);
- renderItemList(_items.filter((i) => i.item_type === type));
+ .forEach((el) => el.classList.remove("active"));
+ document.getElementById("sidebar-all").classList.add("active");
+ updateVaultTitle("All Items");
+ renderItemList(_items);
});
- });
+ document
+ .getElementById("sidebar-security")
+ ?.addEventListener("click", () => switchView("security"));
+ document
+ .getElementById("sidebar-sharing")
+ ?.addEventListener("click", () => switchView("sharing"));
+ document
+ .getElementById("sidebar-emergency")
+ ?.addEventListener("click", () => switchView("emergency"));
+ document
+ .getElementById("sidebar-generator")
+ ?.addEventListener("click", () => openPasswordGeneratorModal());
- document.getElementById("item-modal")?.addEventListener("click", (e) => {
- /* backdrop click intentionally disabled — modal closes only via Cancel or Save */
- });
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- closeModal("share-modal");
+ document.querySelectorAll("[data-type-filter]").forEach((el) => {
+ el.addEventListener("click", () => {
+ const type = el.dataset.typeFilter;
+ const labels = {
+ password: "Passwords",
+ note: "Secure Notes",
+ card: "Payment Cards",
+ bank: "Bank Accounts",
+ address: "Addresses",
+ ssn: "Identities",
+ };
+ _activeFilter = { type: "itemType", value: type };
+ switchView("vault");
+ document
+ .querySelectorAll(".sidebar-item")
+ .forEach((s) => s.classList.remove("active"));
+ el.classList.add("active");
+ updateVaultTitle(labels[type] || type);
+ renderItemList(_items.filter((i) => i.item_type === type));
+ });
+ });
+
+ document.getElementById("item-modal")?.addEventListener("click", (e) => {
+ /* backdrop click intentionally disabled — modal closes only via Cancel or Save */
+ });
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ closeModal("share-modal");
+ closeModal("emergency-modal");
+ closeModal("settings-modal");
+ closeModal("detail-modal");
+ closeModal("gen-modal");
+ }
+ });
+
+ // Detail modal (shared item view)
+ const closeDetailModal = () => closeModal("detail-modal");
+ document
+ .getElementById("btn-close-detail-modal")
+ ?.addEventListener("click", closeDetailModal);
+ document
+ .getElementById("btn-detail-done")
+ ?.addEventListener("click", closeDetailModal);
+ document.getElementById("detail-modal")?.addEventListener("click", (e) => {
+ if (e.target.id === "detail-modal") closeDetailModal();
+ });
+
+ document
+ .getElementById("field-card-number")
+ ?.addEventListener("input", (e) => {
+ let v = e.target.value.replace(/\D/g, "").slice(0, 16);
+ e.target.value = v.replace(/(.{4})/g, "$1 ").trim();
+ });
+
+ // Settings modal
+ document
+ .getElementById("btn-settings")
+ ?.addEventListener("click", openSettingsModal);
+ document
+ .getElementById("btn-close-settings")
+ ?.addEventListener("click", () => closeModal("settings-modal"));
+ document
+ .getElementById("settings-modal")
+ ?.addEventListener("click", (e) => {
+ if (e.target.id === "settings-modal") closeModal("settings-modal");
+ });
+ document
+ .getElementById("btn-mfa-confirm")
+ ?.addEventListener("click", handleMfaConfirm);
+ document
+ .getElementById("btn-mfa-cancel-setup")
+ ?.addEventListener("click", () => {
+ document.getElementById("mfa-setup-area").classList.add("hidden");
+ loadMfaStatus();
+ });
+ document
+ .getElementById("btn-mfa-disable-confirm")
+ ?.addEventListener("click", handleMfaDisableConfirm);
+ document
+ .getElementById("btn-mfa-disable-cancel")
+ ?.addEventListener("click", () => {
+ document.getElementById("mfa-disable-area").classList.add("hidden");
+ loadMfaStatus();
+ });
+ document
+ .getElementById("btn-change-password")
+ ?.addEventListener("click", handleChangePassword);
+ document
+ .getElementById("btn-delete-account")
+ ?.addEventListener("click", () => {
+ document.getElementById("btn-delete-account").classList.add("hidden");
+ document
+ .getElementById("delete-confirm-area")
+ .classList.remove("hidden");
+ document.getElementById("delete-password").focus();
+ });
+ document
+ .getElementById("btn-delete-cancel")
+ ?.addEventListener("click", () => {
+ document.getElementById("delete-confirm-area").classList.add("hidden");
+ document
+ .getElementById("btn-delete-account")
+ .classList.remove("hidden");
+ document.getElementById("delete-password").value = "";
+ document.getElementById("delete-error").classList.add("hidden");
+ });
+ document
+ .getElementById("btn-delete-confirm")
+ ?.addEventListener("click", handleDeleteAccount);
+ document
+ .getElementById("btn-copy-recovery-code")
+ ?.addEventListener("click", () => {
+ const code = document.getElementById("recovery-code-value").textContent;
+ navigator.clipboard
+ .writeText(code)
+ .then(() => showToast("Recovery code copied"));
+ });
+ document
+ .getElementById("btn-recovery-done")
+ ?.addEventListener("click", () => {
+ document
+ .getElementById("recovery-code-display")
+ .classList.add("hidden");
+ loadRecoveryStatus();
+ });
+
+ // Share modal
+ document
+ .getElementById("btn-share-item")
+ ?.addEventListener("click", openShareModal);
+ document
+ .getElementById("btn-close-share-modal")
+ ?.addEventListener("click", () => closeModal("share-modal"));
+ document
+ .getElementById("btn-cancel-share")
+ ?.addEventListener("click", () => closeModal("share-modal"));
+ document.getElementById("share-modal")?.addEventListener("click", (e) => {
+ if (e.target.id === "share-modal") closeModal("share-modal");
+ });
+ document
+ .getElementById("share-form")
+ ?.addEventListener("submit", handleShareFormSubmit);
+ document
+ .getElementById("btn-setup-keys-from-sharing")
+ ?.addEventListener("click", () => {
+ closeModal("share-modal");
+ openSettingsModal();
+ });
+
+ // Emergency modal
+ document
+ .getElementById("btn-add-emergency")
+ ?.addEventListener("click", () => {
+ document.getElementById("emergency-error").classList.add("hidden");
+ document.getElementById("emergency-modal").classList.add("open");
+ });
+ document
+ .getElementById("btn-close-emergency-modal")
+ ?.addEventListener("click", () => closeModal("emergency-modal"));
+ document
+ .getElementById("btn-cancel-emergency")
+ ?.addEventListener("click", () => closeModal("emergency-modal"));
+ document
+ .getElementById("emergency-modal")
+ ?.addEventListener("click", (e) => {
+ if (e.target.id === "emergency-modal") closeModal("emergency-modal");
+ });
+ document
+ .getElementById("emergency-form")
+ ?.addEventListener("submit", handleEmergencyFormSubmit);
+
+ // Tabs
+ document.querySelectorAll(".panel-tabs").forEach((c) => initTabs(c));
+
+ // Sidebar collapse toggle — desktop only (mobile uses hamburger instead)
+ const sidebar = document.getElementById("sidebar");
+ const toggleBtn = document.getElementById("btn-sidebar-toggle");
+ if (
+ sidebar &&
+ toggleBtn &&
+ document.body.classList.contains("is-desktop")
+ ) {
+ const STORAGE_KEY = "sidebar_collapsed";
+ if (localStorage.getItem(STORAGE_KEY) === "1") {
+ sidebar.classList.add("collapsed");
+ toggleBtn.setAttribute("aria-label", "Expand sidebar");
+ toggleBtn.setAttribute("title", "Expand sidebar");
+ }
+ toggleBtn.addEventListener("click", () => {
+ const isCollapsed = sidebar.classList.toggle("collapsed");
+ localStorage.setItem(STORAGE_KEY, isCollapsed ? "1" : "0");
+ toggleBtn.setAttribute(
+ "aria-label",
+ isCollapsed ? "Expand sidebar" : "Collapse sidebar",
+ );
+ toggleBtn.setAttribute(
+ "title",
+ isCollapsed ? "Expand sidebar" : "Collapse sidebar",
+ );
+ });
+ }
+
+ // Mobile sidebar: hamburger opens overlay, backdrop/item-click closes it
+ const backdrop = document.getElementById("sidebar-backdrop");
+ const mobileMenuBtn = document.getElementById("btn-mobile-menu");
+
+ function openMobileSidebar() {
+ sidebar?.classList.add("mobile-open");
+ backdrop?.classList.add("open");
+ document.documentElement.classList.add("sidebar-open");
+ }
+ function closeMobileSidebar() {
+ sidebar?.classList.remove("mobile-open");
+ backdrop?.classList.remove("open");
+ document.documentElement.classList.remove("sidebar-open");
+ }
+
+ mobileMenuBtn?.addEventListener("click", openMobileSidebar);
+ backdrop?.addEventListener("click", closeMobileSidebar);
+ backdrop?.addEventListener("touchstart", closeMobileSidebar, {
+ passive: true,
+ });
+
+ // Close mobile sidebar when a nav item is tapped (mobile only)
+ document.querySelectorAll(".sidebar-item").forEach((item) => {
+ item.addEventListener("click", () => {
+ if (document.body.classList.contains("is-mobile")) closeMobileSidebar();
+ });
+ });
+
+ // Escape key closes the sidebar
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape" && sidebar?.classList.contains("mobile-open"))
+ closeMobileSidebar();
+ });
+
+ // Mobile FAB mirrors the desktop add-item button
+ document
+ .getElementById("btn-add-item-mobile")
+ ?.addEventListener("click", () => {
+ document.getElementById("btn-add-item")?.click();
+ });
+
+ if (!VaultSession.getKey()) {
+ showUnlockOverlay();
+ } else {
+ loadVault();
+ }
+ }
+
+ async function handleEmergencyFormSubmit(e) {
+ e.preventDefault();
+ const errEl = document.getElementById("emergency-error");
+ errEl.classList.add("hidden");
+ const email = document
+ .getElementById("emergency-email")
+ .value.trim()
+ .toLowerCase();
+ const waitDays = document.getElementById("emergency-wait-days").value;
+ if (!email) {
+ errEl.textContent = "Email is required.";
+ errEl.classList.remove("hidden");
+ return;
+ }
+ try {
+ const res = await apiFetch("/api/emergency", {
+ method: "POST",
+ body: JSON.stringify({
+ grantee_email: email,
+ wait_days: parseInt(waitDays),
+ }),
+ });
+ if (!res) return;
closeModal("emergency-modal");
- closeModal("settings-modal");
- closeModal("detail-modal");
- closeModal("gen-modal");
+ showToast("Invitation sent");
+ loadEmergencyView();
+ } catch (err) {
+ errEl.textContent = err.message;
+ errEl.classList.remove("hidden");
}
- });
-
- // Detail modal (shared item view)
- const closeDetailModal = () => closeModal("detail-modal");
- document
- .getElementById("btn-close-detail-modal")
- ?.addEventListener("click", closeDetailModal);
- document
- .getElementById("btn-detail-done")
- ?.addEventListener("click", closeDetailModal);
- document.getElementById("detail-modal")?.addEventListener("click", (e) => {
- if (e.target.id === "detail-modal") closeDetailModal();
- });
-
- document
- .getElementById("field-card-number")
- ?.addEventListener("input", (e) => {
- let v = e.target.value.replace(/\D/g, "").slice(0, 16);
- e.target.value = v.replace(/(.{4})/g, "$1 ").trim();
- });
-
- // Settings modal
- document
- .getElementById("btn-settings")
- ?.addEventListener("click", openSettingsModal);
- document
- .getElementById("btn-close-settings")
- ?.addEventListener("click", () => closeModal("settings-modal"));
- document
- .getElementById("settings-modal")
- ?.addEventListener("click", (e) => {
- if (e.target.id === "settings-modal") closeModal("settings-modal");
- });
- document
- .getElementById("btn-mfa-confirm")
- ?.addEventListener("click", handleMfaConfirm);
- document
- .getElementById("btn-mfa-cancel-setup")
- ?.addEventListener("click", () => {
- document.getElementById("mfa-setup-area").classList.add("hidden");
- loadMfaStatus();
- });
- document
- .getElementById("btn-mfa-disable-confirm")
- ?.addEventListener("click", handleMfaDisableConfirm);
- document
- .getElementById("btn-mfa-disable-cancel")
- ?.addEventListener("click", () => {
- document.getElementById("mfa-disable-area").classList.add("hidden");
- loadMfaStatus();
- });
- document
- .getElementById("btn-change-password")
- ?.addEventListener("click", handleChangePassword);
- document
- .getElementById("btn-delete-account")
- ?.addEventListener("click", () => {
- document.getElementById("btn-delete-account").classList.add("hidden");
- document
- .getElementById("delete-confirm-area")
- .classList.remove("hidden");
- document.getElementById("delete-password").focus();
- });
- document
- .getElementById("btn-delete-cancel")
- ?.addEventListener("click", () => {
- document.getElementById("delete-confirm-area").classList.add("hidden");
- document
- .getElementById("btn-delete-account")
- .classList.remove("hidden");
- document.getElementById("delete-password").value = "";
- document.getElementById("delete-error").classList.add("hidden");
- });
- document
- .getElementById("btn-delete-confirm")
- ?.addEventListener("click", handleDeleteAccount);
- document
- .getElementById("btn-copy-recovery-code")
- ?.addEventListener("click", () => {
- const code = document.getElementById("recovery-code-value").textContent;
- navigator.clipboard
- .writeText(code)
- .then(() => showToast("Recovery code copied"));
- });
- document
- .getElementById("btn-recovery-done")
- ?.addEventListener("click", () => {
- document
- .getElementById("recovery-code-display")
- .classList.add("hidden");
- loadRecoveryStatus();
- });
-
- // Share modal
- document
- .getElementById("btn-share-item")
- ?.addEventListener("click", openShareModal);
- document
- .getElementById("btn-close-share-modal")
- ?.addEventListener("click", () => closeModal("share-modal"));
- document
- .getElementById("btn-cancel-share")
- ?.addEventListener("click", () => closeModal("share-modal"));
- document.getElementById("share-modal")?.addEventListener("click", (e) => {
- if (e.target.id === "share-modal") closeModal("share-modal");
- });
- document
- .getElementById("share-form")
- ?.addEventListener("submit", handleShareFormSubmit);
- document
- .getElementById("btn-setup-keys-from-sharing")
- ?.addEventListener("click", () => {
- closeModal("share-modal");
- openSettingsModal();
- });
-
- // Emergency modal
- document
- .getElementById("btn-add-emergency")
- ?.addEventListener("click", () => {
- document.getElementById("emergency-error").classList.add("hidden");
- document.getElementById("emergency-modal").classList.add("open");
- });
- document
- .getElementById("btn-close-emergency-modal")
- ?.addEventListener("click", () => closeModal("emergency-modal"));
- document
- .getElementById("btn-cancel-emergency")
- ?.addEventListener("click", () => closeModal("emergency-modal"));
- document
- .getElementById("emergency-modal")
- ?.addEventListener("click", (e) => {
- if (e.target.id === "emergency-modal") closeModal("emergency-modal");
- });
- document
- .getElementById("emergency-form")
- ?.addEventListener("submit", handleEmergencyFormSubmit);
-
- // Tabs
- document.querySelectorAll(".panel-tabs").forEach((c) => initTabs(c));
-
- // Sidebar collapse toggle — desktop only (mobile uses hamburger instead)
- const sidebar = document.getElementById("sidebar");
- const toggleBtn = document.getElementById("btn-sidebar-toggle");
- if (
- sidebar &&
- toggleBtn &&
- document.body.classList.contains("is-desktop")
- ) {
- const STORAGE_KEY = "sidebar_collapsed";
- if (localStorage.getItem(STORAGE_KEY) === "1") {
- sidebar.classList.add("collapsed");
- toggleBtn.setAttribute("aria-label", "Expand sidebar");
- toggleBtn.setAttribute("title", "Expand sidebar");
- }
- toggleBtn.addEventListener("click", () => {
- const isCollapsed = sidebar.classList.toggle("collapsed");
- localStorage.setItem(STORAGE_KEY, isCollapsed ? "1" : "0");
- toggleBtn.setAttribute(
- "aria-label",
- isCollapsed ? "Expand sidebar" : "Collapse sidebar",
- );
- toggleBtn.setAttribute(
- "title",
- isCollapsed ? "Expand sidebar" : "Collapse sidebar",
- );
- });
}
- // Mobile sidebar: hamburger opens overlay, backdrop/item-click closes it
- const backdrop = document.getElementById("sidebar-backdrop");
- const mobileMenuBtn = document.getElementById("btn-mobile-menu");
-
- function openMobileSidebar() {
- sidebar?.classList.add("mobile-open");
- backdrop?.classList.add("open");
- document.documentElement.style.overflow = "hidden";
- }
- function closeMobileSidebar() {
- sidebar?.classList.remove("mobile-open");
- backdrop?.classList.remove("open");
- document.documentElement.style.overflow = "";
- }
-
- mobileMenuBtn?.addEventListener("click", openMobileSidebar);
- backdrop?.addEventListener("click", closeMobileSidebar);
- backdrop?.addEventListener("touchstart", closeMobileSidebar, {
- passive: true,
- });
-
- // Close mobile sidebar when a nav item is tapped (mobile only)
- document.querySelectorAll(".sidebar-item").forEach((item) => {
- item.addEventListener("click", () => {
- if (document.body.classList.contains("is-mobile")) closeMobileSidebar();
- });
- });
-
- // Escape key closes the sidebar
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && sidebar?.classList.contains("mobile-open"))
- closeMobileSidebar();
- });
-
- // Mobile FAB mirrors the desktop add-item button
- document
- .getElementById("btn-add-item-mobile")
- ?.addEventListener("click", () => {
- document.getElementById("btn-add-item")?.click();
- });
-
- if (!VaultSession.getKey()) {
- showUnlockOverlay();
- } else {
- loadVault();
- }
-}
-
-async function handleEmergencyFormSubmit(e) {
- e.preventDefault();
- const errEl = document.getElementById("emergency-error");
- errEl.classList.add("hidden");
- const email = document
- .getElementById("emergency-email")
- .value.trim()
- .toLowerCase();
- const waitDays = document.getElementById("emergency-wait-days").value;
- if (!email) {
- errEl.textContent = "Email is required.";
- errEl.classList.remove("hidden");
- return;
- }
- try {
- const res = await apiFetch("/api/emergency", {
- method: "POST",
- body: JSON.stringify({
- grantee_email: email,
- wait_days: parseInt(waitDays),
- }),
- });
- if (!res) return;
- closeModal("emergency-modal");
- showToast("Invitation sent");
- loadEmergencyView();
- } catch (err) {
- errEl.textContent = err.message;
- errEl.classList.remove("hidden");
- }
-}
-
-return { init };
-}) ();
+ return { init };
+})();
/**
* Mobile/desktop mode detection.
@@ -2863,7 +2885,7 @@ return { init };
if (!e.matches) {
document.getElementById("sidebar")?.classList.remove("mobile-open");
document.getElementById("sidebar-backdrop")?.classList.remove("open");
- document.documentElement.style.overflow = "";
+ document.documentElement.classList.remove("sidebar-open");
}
});
})();
@@ -2892,4 +2914,4 @@ return { init };
}
})();
-document.addEventListener("DOMContentLoaded", Vault.init);
\ No newline at end of file
+document.addEventListener("DOMContentLoaded", Vault.init);
diff --git a/app/templates/auth/recover.html b/app/templates/auth/recover.html
index 60ad0de..462bbb7 100644
--- a/app/templates/auth/recover.html
+++ b/app/templates/auth/recover.html
@@ -1,8 +1,5 @@
-{% extends "base.html" %}
-{% block title %}Recover Account — PassKeeper{% endblock %}
-{% block body_class %}auth-page{% endblock %}
-
-{% block body %}
+{% extends "base.html" %} {% block title %}Recover Account — PassKeeper{%
+endblock %} {% block body_class %}auth-page{% endblock %} {% block body %}
@@ -13,21 +10,40 @@
Recover your account
-
Enter your email and the recovery code you saved when you set up account recovery.
+
+ Enter your email and the recovery code you saved when you set up account
+ recovery.
+
@@ -39,34 +55,56 @@
Set a new master password
-
Your recovery code is valid. Choose a new master password — your vault will be re-encrypted automatically.
+
+ Your recovery code is valid. Choose a new master password — your vault
+ will be re-encrypted automatically.
+
-
-{% endblock %}
-
-{% block scripts %}
+{% endblock %} {% block scripts %}
{% endblock %}
diff --git a/app/templates/vault/index.html b/app/templates/vault/index.html
index 24496b6..4e0b6d1 100644
--- a/app/templates/vault/index.html
+++ b/app/templates/vault/index.html
@@ -7,7 +7,11 @@
☰
🔒 PassKeeper
-
@@ -20,56 +24,109 @@