04/20/2026 enhance the extension totp
This commit is contained in:
+424
-1250
File diff suppressed because it is too large
Load Diff
+1689
-785
File diff suppressed because it is too large
Load Diff
+89
-344
@@ -7,11 +7,7 @@
|
|||||||
☰
|
☰
|
||||||
</button>
|
</button>
|
||||||
<span class="mobile-logo">🔒 PassKeeper</span>
|
<span class="mobile-logo">🔒 PassKeeper</span>
|
||||||
<button
|
<button class="btn-fab btn-fab-mobile" id="btn-add-item-mobile" title="Add item">
|
||||||
class="btn-fab btn-fab-mobile"
|
|
||||||
id="btn-add-item-mobile"
|
|
||||||
title="Add item"
|
|
||||||
>
|
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,109 +20,56 @@
|
|||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<span class="logo-icon">🔒</span>
|
<span class="logo-icon">🔒</span>
|
||||||
<span class="logo-text">PassKeeper</span>
|
<span class="logo-text">PassKeeper</span>
|
||||||
<button
|
<button class="btn-sidebar-toggle" id="btn-sidebar-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
|
||||||
class="btn-sidebar-toggle"
|
|
||||||
id="btn-sidebar-toggle"
|
|
||||||
aria-label="Collapse sidebar"
|
|
||||||
title="Collapse sidebar"
|
|
||||||
>
|
|
||||||
❮
|
❮
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li class="sidebar-item active" id="sidebar-all" data-view="vault" data-tooltip="All Items">
|
||||||
class="sidebar-item active"
|
|
||||||
id="sidebar-all"
|
|
||||||
data-view="vault"
|
|
||||||
data-tooltip="All Items"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🏠</span>
|
<span class="sidebar-icon">🏠</span>
|
||||||
<span class="sidebar-label">All Items</span>
|
<span class="sidebar-label">All Items</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-section-header">Item Types</li>
|
<li class="sidebar-section-header">Item Types</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="password" data-tooltip="Passwords">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="password"
|
|
||||||
data-tooltip="Passwords"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🔑</span>
|
<span class="sidebar-icon">🔑</span>
|
||||||
<span class="sidebar-label">Passwords</span>
|
<span class="sidebar-label">Passwords</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="note" data-tooltip="Secure Notes">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="note"
|
|
||||||
data-tooltip="Secure Notes"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">📝</span>
|
<span class="sidebar-icon">📝</span>
|
||||||
<span class="sidebar-label">Secure Notes</span>
|
<span class="sidebar-label">Secure Notes</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="card" data-tooltip="Payment Cards">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="card"
|
|
||||||
data-tooltip="Payment Cards"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">💳</span>
|
<span class="sidebar-icon">💳</span>
|
||||||
<span class="sidebar-label">Payment Cards</span>
|
<span class="sidebar-label">Payment Cards</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="bank" data-tooltip="Bank Accounts">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="bank"
|
|
||||||
data-tooltip="Bank Accounts"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🏦</span>
|
<span class="sidebar-icon">🏦</span>
|
||||||
<span class="sidebar-label">Bank Accounts</span>
|
<span class="sidebar-label">Bank Accounts</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="address" data-tooltip="Addresses">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="address"
|
|
||||||
data-tooltip="Addresses"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🏠</span>
|
<span class="sidebar-icon">🏠</span>
|
||||||
<span class="sidebar-label">Addresses</span>
|
<span class="sidebar-label">Addresses</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" data-type-filter="ssn" data-tooltip="Identities">
|
||||||
class="sidebar-item"
|
|
||||||
data-type-filter="ssn"
|
|
||||||
data-tooltip="Identities"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🪪</span>
|
<span class="sidebar-icon">🪪</span>
|
||||||
<span class="sidebar-label">Identities</span>
|
<span class="sidebar-label">Identities</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-section-header">Tools</li>
|
<li class="sidebar-section-header">Tools</li>
|
||||||
<li
|
<li class="sidebar-item" id="sidebar-security" data-view="security" data-tooltip="Security">
|
||||||
class="sidebar-item"
|
|
||||||
id="sidebar-security"
|
|
||||||
data-view="security"
|
|
||||||
data-tooltip="Security"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🛡️</span>
|
<span class="sidebar-icon">🛡️</span>
|
||||||
<span class="sidebar-label">Security</span>
|
<span class="sidebar-label">Security</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" id="sidebar-sharing" data-view="sharing" data-tooltip="Sharing">
|
||||||
class="sidebar-item"
|
|
||||||
id="sidebar-sharing"
|
|
||||||
data-view="sharing"
|
|
||||||
data-tooltip="Sharing"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🔗</span>
|
<span class="sidebar-icon">🔗</span>
|
||||||
<span class="sidebar-label">Sharing</span>
|
<span class="sidebar-label">Sharing</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" id="sidebar-emergency" data-view="emergency" data-tooltip="Emergency Access">
|
||||||
class="sidebar-item"
|
|
||||||
id="sidebar-emergency"
|
|
||||||
data-view="emergency"
|
|
||||||
data-tooltip="Emergency Access"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🚨</span>
|
<span class="sidebar-icon">🚨</span>
|
||||||
<span class="sidebar-label">Emergency Access</span>
|
<span class="sidebar-label">Emergency Access</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="sidebar-item" id="sidebar-generator" data-tooltip="Password Generator">
|
||||||
class="sidebar-item"
|
|
||||||
id="sidebar-generator"
|
|
||||||
data-tooltip="Password Generator"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">⚡</span>
|
<span class="sidebar-icon">⚡</span>
|
||||||
<span class="sidebar-label">Password Generator</span>
|
<span class="sidebar-label">Password Generator</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -138,19 +81,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<li id="new-folder-row" class="new-folder-row hidden">
|
<li id="new-folder-row" class="new-folder-row hidden">
|
||||||
<form id="new-folder-form">
|
<form id="new-folder-form">
|
||||||
<input
|
<input type="text" id="new-folder-input" placeholder="Folder name" maxlength="128" />
|
||||||
type="text"
|
|
||||||
id="new-folder-input"
|
|
||||||
placeholder="Folder name"
|
|
||||||
maxlength="128"
|
|
||||||
/>
|
|
||||||
<button type="submit" class="btn-icon" title="Save">✓</button>
|
<button type="submit" class="btn-icon" title="Save">✓</button>
|
||||||
<button
|
<button type="button" class="btn-icon" id="btn-cancel-folder" title="Cancel">
|
||||||
type="button"
|
|
||||||
class="btn-icon"
|
|
||||||
id="btn-cancel-folder"
|
|
||||||
title="Cancel"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -160,19 +93,11 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button
|
<button class="sidebar-item btn-text" id="btn-settings" data-tooltip="Settings">
|
||||||
class="sidebar-item btn-text"
|
|
||||||
id="btn-settings"
|
|
||||||
data-tooltip="Settings"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">⚙️</span>
|
<span class="sidebar-icon">⚙️</span>
|
||||||
<span class="sidebar-label">Settings</span>
|
<span class="sidebar-label">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="sidebar-item btn-text" id="btn-logout" data-tooltip="Sign out">
|
||||||
class="sidebar-item btn-text"
|
|
||||||
id="btn-logout"
|
|
||||||
data-tooltip="Sign out"
|
|
||||||
>
|
|
||||||
<span class="sidebar-icon">🚪</span>
|
<span class="sidebar-icon">🚪</span>
|
||||||
<span class="sidebar-label">Sign out</span>
|
<span class="sidebar-label">Sign out</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -189,12 +114,7 @@
|
|||||||
<div class="vault-toolbar">
|
<div class="vault-toolbar">
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<span class="search-icon">🔍</span>
|
<span class="search-icon">🔍</span>
|
||||||
<input
|
<input type="search" id="search-input" placeholder="Search vault…" autocomplete="off" />
|
||||||
type="search"
|
|
||||||
id="search-input"
|
|
||||||
placeholder="Search vault…"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sort-wrapper">
|
<div class="sort-wrapper">
|
||||||
<select id="sort-select" title="Sort by">
|
<select id="sort-select" title="Sort by">
|
||||||
@@ -304,13 +224,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Unlock overlay ──────────────────────────────────────────────── -->
|
<!-- ── Unlock overlay ──────────────────────────────────────────────── -->
|
||||||
<div
|
<div id="unlock-overlay" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="unlock-title">
|
||||||
id="unlock-overlay"
|
|
||||||
class="modal-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="unlock-title"
|
|
||||||
>
|
|
||||||
<div class="modal modal-unlock">
|
<div class="modal modal-unlock">
|
||||||
<div class="auth-logo unlock-logo">
|
<div class="auth-logo unlock-logo">
|
||||||
<span class="logo-icon">🔒</span><span class="logo-text">PassKeeper</span>
|
<span class="logo-icon">🔒</span><span class="logo-text">PassKeeper</span>
|
||||||
@@ -323,12 +237,8 @@
|
|||||||
<form id="unlock-form" novalidate>
|
<form id="unlock-form" novalidate>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="unlock-password">Master password</label>
|
<label for="unlock-password">Master password</label>
|
||||||
<input
|
<input type="password" id="unlock-password" autocomplete="current-password"
|
||||||
type="password"
|
placeholder="Enter master password" />
|
||||||
id="unlock-password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Enter master password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<a href="/login" class="btn-secondary btn-link">Sign out</a>
|
<a href="/login" class="btn-secondary btn-link">Sign out</a>
|
||||||
@@ -339,21 +249,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Add / Edit Item Modal ─────────────────────────────────────────── -->
|
<!-- ── Add / Edit Item Modal ─────────────────────────────────────────── -->
|
||||||
<div
|
<div id="item-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||||
id="item-modal"
|
|
||||||
class="modal-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="modal-title"
|
|
||||||
>
|
|
||||||
<div class="modal modal-item">
|
<div class="modal modal-item">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modal-title">Add Item</h3>
|
<h3 id="modal-title">Add Item</h3>
|
||||||
<button
|
<button class="btn-icon btn-close" id="btn-close-modal" aria-label="Close">
|
||||||
class="btn-icon btn-close"
|
|
||||||
id="btn-close-modal"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,76 +271,49 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-name">Name <span class="required">*</span></label>
|
<label for="field-name">Name <span class="required">*</span></label>
|
||||||
<input
|
<input type="text" id="field-name" name="name" required placeholder="e.g. Gmail" />
|
||||||
type="text"
|
|
||||||
id="field-name"
|
|
||||||
name="name"
|
|
||||||
required
|
|
||||||
placeholder="e.g. Gmail"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="type-fields" data-for-types="password">
|
<div class="type-fields" data-for-types="password">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-url">Website URL</label
|
<label for="field-url">Website URL</label><input type="url" id="field-url"
|
||||||
><input type="url" id="field-url" placeholder="https://example.com" />
|
placeholder="https://example.com" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-username">Username / Email</label
|
<label for="field-username">Username / Email</label><input type="text" id="field-username" autocomplete="off"
|
||||||
><input
|
placeholder="username or email" />
|
||||||
type="text"
|
|
||||||
id="field-username"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="username or email"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-password">Password</label>
|
<label for="field-password">Password</label>
|
||||||
<div class="input-with-toggle">
|
<div class="input-with-toggle">
|
||||||
<input
|
<input type="password" id="field-password" autocomplete="new-password" placeholder="Enter password" />
|
||||||
type="password"
|
<button type="button" class="btn-show-pass" id="btn-toggle-pass" aria-label="Toggle">
|
||||||
id="field-password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Enter password"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-show-pass"
|
|
||||||
id="btn-toggle-pass"
|
|
||||||
aria-label="Toggle"
|
|
||||||
>
|
|
||||||
👁
|
👁
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="field-totp-uri">Authenticator Key (TOTP) <span
|
||||||
|
style="font-size:11px;color:#9ca3af;font-weight:400;">optional</span></label>
|
||||||
|
<input type="text" id="field-totp-uri" autocomplete="off"
|
||||||
|
placeholder="otpauth://totp/… or plain base32 secret" />
|
||||||
|
<p style="font-size:11px;color:#6b7280;margin-top:4px;">Paste the otpauth URI from a QR code scanner, or the
|
||||||
|
plain base32 secret key. PassKeeper will generate your 2FA codes.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-fields hidden" data-for-types="note">
|
<div class="type-fields hidden" data-for-types="note">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-note-body">Note</label
|
<label for="field-note-body">Note</label><textarea id="field-note-body" rows="6"
|
||||||
><textarea
|
placeholder="Enter your secure note…"></textarea>
|
||||||
id="field-note-body"
|
|
||||||
rows="6"
|
|
||||||
placeholder="Enter your secure note…"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-fields hidden" data-for-types="card">
|
<div class="type-fields hidden" data-for-types="card">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-cardholder">Cardholder Name</label
|
<label for="field-cardholder">Cardholder Name</label><input type="text" id="field-cardholder"
|
||||||
><input
|
placeholder="Name on card" />
|
||||||
type="text"
|
|
||||||
id="field-cardholder"
|
|
||||||
placeholder="Name on card"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-card-number">Card Number</label
|
<label for="field-card-number">Card Number</label><input type="text" id="field-card-number" autocomplete="off"
|
||||||
><input
|
placeholder="•••• •••• •••• ••••" maxlength="19" />
|
||||||
type="text"
|
|
||||||
id="field-card-number"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="•••• •••• •••• ••••"
|
|
||||||
maxlength="19"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -462,30 +335,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-expiry-year">Year</label
|
<label for="field-expiry-year">Year</label><input type="text" id="field-expiry-year" placeholder="YYYY"
|
||||||
><input
|
maxlength="4" />
|
||||||
type="text"
|
|
||||||
id="field-expiry-year"
|
|
||||||
placeholder="YYYY"
|
|
||||||
maxlength="4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-cvv">CVV</label
|
<label for="field-cvv">CVV</label><input type="text" id="field-cvv" autocomplete="off" placeholder="•••"
|
||||||
><input
|
maxlength="4" />
|
||||||
type="text"
|
|
||||||
id="field-cvv"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="•••"
|
|
||||||
maxlength="4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-fields hidden" data-for-types="bank">
|
<div class="type-fields hidden" data-for-types="bank">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-bank-name">Bank Name</label
|
<label for="field-bank-name">Bank Name</label><input type="text" id="field-bank-name"
|
||||||
><input type="text" id="field-bank-name" placeholder="e.g. Chase" />
|
placeholder="e.g. Chase" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-account-type">Account Type</label>
|
<label for="field-account-type">Account Type</label>
|
||||||
@@ -498,88 +360,56 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-routing">Routing Number</label
|
<label for="field-routing">Routing Number</label><input type="text" id="field-routing" autocomplete="off"
|
||||||
><input
|
placeholder="9-digit routing number" maxlength="9" />
|
||||||
type="text"
|
|
||||||
id="field-routing"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="9-digit routing number"
|
|
||||||
maxlength="9"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-account-number">Account Number</label
|
<label for="field-account-number">Account Number</label><input type="text" id="field-account-number"
|
||||||
><input
|
autocomplete="off" placeholder="Account number" />
|
||||||
type="text"
|
|
||||||
id="field-account-number"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Account number"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-fields hidden" data-for-types="address">
|
<div class="type-fields hidden" data-for-types="address">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-first-name">First Name</label
|
<label for="field-first-name">First Name</label><input type="text" id="field-first-name"
|
||||||
><input
|
placeholder="First name" />
|
||||||
type="text"
|
|
||||||
id="field-first-name"
|
|
||||||
placeholder="First name"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-last-name">Last Name</label
|
<label for="field-last-name">Last Name</label><input type="text" id="field-last-name"
|
||||||
><input type="text" id="field-last-name" placeholder="Last name" />
|
placeholder="Last name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-company">Company</label
|
<label for="field-company">Company</label><input type="text" id="field-company"
|
||||||
><input
|
placeholder="Company (optional)" />
|
||||||
type="text"
|
|
||||||
id="field-company"
|
|
||||||
placeholder="Company (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-address-line">Street Address</label
|
<label for="field-address-line">Street Address</label><input type="text" id="field-address-line"
|
||||||
><input
|
placeholder="123 Main St" />
|
||||||
type="text"
|
|
||||||
id="field-address-line"
|
|
||||||
placeholder="123 Main St"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-city">City</label
|
<label for="field-city">City</label><input type="text" id="field-city" placeholder="City" />
|
||||||
><input type="text" id="field-city" placeholder="City" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-state">State</label
|
<label for="field-state">State</label><input type="text" id="field-state" placeholder="State" />
|
||||||
><input type="text" id="field-state" placeholder="State" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-zip">ZIP</label
|
<label for="field-zip">ZIP</label><input type="text" id="field-zip" placeholder="ZIP" />
|
||||||
><input type="text" id="field-zip" placeholder="ZIP" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-country">Country</label
|
<label for="field-country">Country</label><input type="text" id="field-country" placeholder="Country" />
|
||||||
><input type="text" id="field-country" placeholder="Country" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-phone">Phone</label
|
<label for="field-phone">Phone</label><input type="tel" id="field-phone" placeholder="+1 555-000-0000" />
|
||||||
><input type="tel" id="field-phone" placeholder="+1 555-000-0000" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-email">Email</label
|
<label for="field-email">Email</label><input type="email" id="field-email"
|
||||||
><input
|
placeholder="email@example.com" />
|
||||||
type="email"
|
|
||||||
id="field-email"
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -587,19 +417,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-ssn-number">Social Security Number</label>
|
<label for="field-ssn-number">Social Security Number</label>
|
||||||
<div class="input-with-toggle">
|
<div class="input-with-toggle">
|
||||||
<input
|
<input type="password" id="field-ssn-number" autocomplete="off" placeholder="•••-••-••••" maxlength="11" />
|
||||||
type="password"
|
<button type="button" class="btn-show-pass" id="btn-toggle-ssn" aria-label="Toggle">
|
||||||
id="field-ssn-number"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="•••-••-••••"
|
|
||||||
maxlength="11"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-show-pass"
|
|
||||||
id="btn-toggle-ssn"
|
|
||||||
aria-label="Toggle"
|
|
||||||
>
|
|
||||||
👁
|
👁
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,12 +432,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="type-fields" data-for-types="password card bank address ssn">
|
<div class="type-fields" data-for-types="password card bank address ssn">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="field-notes">Notes</label
|
<label for="field-notes">Notes</label><textarea id="field-notes" rows="3"
|
||||||
><textarea
|
placeholder="Optional notes…"></textarea>
|
||||||
id="field-notes"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Optional notes…"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -636,11 +451,7 @@
|
|||||||
<div class="modal modal-settings">
|
<div class="modal modal-settings">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Account Settings</h3>
|
<h3>Account Settings</h3>
|
||||||
<button
|
<button class="btn-icon btn-close" id="btn-close-settings" aria-label="Close">
|
||||||
class="btn-icon btn-close"
|
|
||||||
id="btn-close-settings"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -661,13 +472,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="mfa-verify-code">Verification code</label>
|
<label for="mfa-verify-code">Verification code</label>
|
||||||
<input
|
<input type="text" id="mfa-verify-code" inputmode="numeric" maxlength="6" placeholder="000000" />
|
||||||
type="text"
|
|
||||||
id="mfa-verify-code"
|
|
||||||
inputmode="numeric"
|
|
||||||
maxlength="6"
|
|
||||||
placeholder="000000"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="btn-mfa-confirm">Enable MFA</button>
|
<button class="btn-primary" id="btn-mfa-confirm">Enable MFA</button>
|
||||||
<button class="btn-secondary" id="btn-mfa-cancel-setup">
|
<button class="btn-secondary" id="btn-mfa-cancel-setup">
|
||||||
@@ -676,16 +481,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="mfa-disable-area" class="hidden">
|
<div id="mfa-disable-area" class="hidden">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="mfa-disable-code"
|
<label for="mfa-disable-code">Enter your current code to disable MFA</label>
|
||||||
>Enter your current code to disable MFA</label
|
<input type="text" id="mfa-disable-code" inputmode="numeric" maxlength="6" placeholder="000000" />
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="mfa-disable-code"
|
|
||||||
inputmode="numeric"
|
|
||||||
maxlength="6"
|
|
||||||
placeholder="000000"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="btn-mfa-disable-confirm">
|
<button class="btn-primary" id="btn-mfa-disable-confirm">
|
||||||
Disable MFA
|
Disable MFA
|
||||||
@@ -716,30 +513,15 @@
|
|||||||
<div id="change-password-area">
|
<div id="change-password-area">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cp-current">Current master password</label>
|
<label for="cp-current">Current master password</label>
|
||||||
<input
|
<input type="password" id="cp-current" autocomplete="current-password" placeholder="Current password" />
|
||||||
type="password"
|
|
||||||
id="cp-current"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Current password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cp-new">New master password</label>
|
<label for="cp-new">New master password</label>
|
||||||
<input
|
<input type="password" id="cp-new" autocomplete="new-password" placeholder="Minimum 12 characters" />
|
||||||
type="password"
|
|
||||||
id="cp-new"
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Minimum 12 characters"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cp-confirm">Confirm new password</label>
|
<label for="cp-confirm">Confirm new password</label>
|
||||||
<input
|
<input type="password" id="cp-confirm" autocomplete="new-password" placeholder="Repeat new password" />
|
||||||
type="password"
|
|
||||||
id="cp-confirm"
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Repeat new password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="cp-error" class="form-error hidden"></p>
|
<p id="cp-error" class="form-error hidden"></p>
|
||||||
<button class="btn-primary" id="btn-change-password">
|
<button class="btn-primary" id="btn-change-password">
|
||||||
@@ -760,11 +542,7 @@
|
|||||||
<div id="recovery-code-display" class="hidden">
|
<div id="recovery-code-display" class="hidden">
|
||||||
<div class="recovery-code-box">
|
<div class="recovery-code-box">
|
||||||
<code id="recovery-code-value"></code>
|
<code id="recovery-code-value"></code>
|
||||||
<button
|
<button class="btn-icon" id="btn-copy-recovery-code" title="Copy to clipboard">
|
||||||
class="btn-icon"
|
|
||||||
id="btn-copy-recovery-code"
|
|
||||||
title="Copy to clipboard"
|
|
||||||
>
|
|
||||||
📋
|
📋
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -788,15 +566,8 @@
|
|||||||
<div id="delete-account-area">
|
<div id="delete-account-area">
|
||||||
<div id="delete-confirm-area" class="hidden">
|
<div id="delete-confirm-area" class="hidden">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="delete-password"
|
<label for="delete-password">Enter your master password to confirm</label>
|
||||||
>Enter your master password to confirm</label
|
<input type="password" id="delete-password" autocomplete="current-password" placeholder="Master password" />
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="delete-password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Master password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="delete-error" class="form-error hidden"></p>
|
<p id="delete-error" class="form-error hidden"></p>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -819,11 +590,7 @@
|
|||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Share an Item</h3>
|
<h3>Share an Item</h3>
|
||||||
<button
|
<button class="btn-icon btn-close" id="btn-close-share-modal" aria-label="Close">
|
||||||
class="btn-icon btn-close"
|
|
||||||
id="btn-close-share-modal"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -835,11 +602,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="share-recipient-email">Recipient's email</label>
|
<label for="share-recipient-email">Recipient's email</label>
|
||||||
<input
|
<input type="email" id="share-recipient-email" placeholder="recipient@example.com" />
|
||||||
type="email"
|
|
||||||
id="share-recipient-email"
|
|
||||||
placeholder="recipient@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button type="button" class="btn-secondary" id="btn-cancel-share">
|
<button type="button" class="btn-secondary" id="btn-cancel-share">
|
||||||
@@ -856,11 +619,7 @@
|
|||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Add Emergency Contact</h3>
|
<h3>Add Emergency Contact</h3>
|
||||||
<button
|
<button class="btn-icon btn-close" id="btn-close-emergency-modal" aria-label="Close">
|
||||||
class="btn-icon btn-close"
|
|
||||||
id="btn-close-emergency-modal"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -872,11 +631,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="emergency-email">Trusted contact's email</label>
|
<label for="emergency-email">Trusted contact's email</label>
|
||||||
<input
|
<input type="email" id="emergency-email" placeholder="contact@example.com" />
|
||||||
type="email"
|
|
||||||
id="emergency-email"
|
|
||||||
placeholder="contact@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="emergency-wait-days">Wait period (days)</label>
|
<label for="emergency-wait-days">Wait period (days)</label>
|
||||||
@@ -899,21 +654,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Item Detail Modal (read-only: shared items) ────────────────────── -->
|
<!-- ── Item Detail Modal (read-only: shared items) ────────────────────── -->
|
||||||
<div
|
<div id="detail-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="detail-modal-title">
|
||||||
id="detail-modal"
|
|
||||||
class="modal-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="detail-modal-title"
|
|
||||||
>
|
|
||||||
<div class="modal modal-detail">
|
<div class="modal modal-detail">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="detail-modal-title">Item Details</h3>
|
<h3 id="detail-modal-title">Item Details</h3>
|
||||||
<button
|
<button class="btn-icon btn-close" id="btn-close-detail-modal" aria-label="Close">
|
||||||
class="btn-icon btn-close"
|
|
||||||
id="btn-close-detail-modal"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -902,3 +902,33 @@ body {
|
|||||||
border-color: #c0392b;
|
border-color: #c0392b;
|
||||||
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
|
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── TOTP live display in vault item rows ────────────────────────── */
|
||||||
|
.item-totp-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-code-inline {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a73e8;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-timer-inline {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-btn {
|
||||||
|
color: #0891b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-btn:hover {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
+105
-1
@@ -342,6 +342,68 @@ async function fetchAndDecryptVault() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TOTP engine (RFC 6238) — pure Web Crypto, no library ─────────────────────
|
||||||
|
|
||||||
|
function _extractTotpSecret(uri) {
|
||||||
|
if (!uri) return null;
|
||||||
|
uri = uri.trim();
|
||||||
|
if (uri.startsWith('otpauth://')) {
|
||||||
|
try {
|
||||||
|
const secret = new URL(uri).searchParams.get('secret');
|
||||||
|
return secret ? secret.toUpperCase().replace(/\s+/g, '') : null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
return uri.toUpperCase().replace(/[\s-]/g, '') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _base32ToBytes(b32) {
|
||||||
|
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
b32 = b32.replace(/=+$/, '');
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
return new Uint8Array(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTotpCode(totpUri) {
|
||||||
|
const secret = _extractTotpSecret(totpUri);
|
||||||
|
if (!secret) return null;
|
||||||
|
const keyBytes = _base32ToBytes(secret);
|
||||||
|
if (!keyBytes.length) return null;
|
||||||
|
|
||||||
|
const counter = Math.floor(Date.now() / 1000 / 30);
|
||||||
|
const msg = new Uint8Array(8);
|
||||||
|
let c = counter;
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
return String(code).padStart(6, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _totpSecondsLeft() {
|
||||||
|
return 30 - (Math.floor(Date.now() / 1000) % 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track active TOTP ticker intervals so we can clear them on re-render.
|
||||||
|
let _totpIntervals = [];
|
||||||
|
|
||||||
|
function _clearTotpTickers() {
|
||||||
|
_totpIntervals.forEach(id => clearInterval(id));
|
||||||
|
_totpIntervals = [];
|
||||||
|
}
|
||||||
|
|
||||||
// ── Rendering ─────────────────────────────────────────────────────────────────
|
// ── Rendering ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getTabItems() {
|
function getTabItems() {
|
||||||
@@ -375,6 +437,9 @@ function renderList() {
|
|||||||
const emptyEl = $('vault-empty');
|
const emptyEl = $('vault-empty');
|
||||||
const items = getTabItems();
|
const items = getTabItems();
|
||||||
|
|
||||||
|
// Stop any running TOTP tickers from a previous render.
|
||||||
|
_clearTotpTickers();
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (_activeTab === 'relevant') {
|
if (_activeTab === 'relevant') {
|
||||||
@@ -394,6 +459,7 @@ function renderList() {
|
|||||||
const svgCopy = `<svg viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke="currentColor" stroke-width="1.7"/></svg>`;
|
const svgCopy = `<svg viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke="currentColor" stroke-width="1.7"/></svg>`;
|
||||||
const svgDots = `<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>`;
|
const svgDots = `<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>`;
|
||||||
const svgFill = `<svg viewBox="0 0 24 24" fill="none"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M17 3l4 4-9 9H8v-4l9-9z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>`;
|
const svgFill = `<svg viewBox="0 0 24 24" fill="none"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M17 3l4 4-9 9H8v-4l9-9z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>`;
|
||||||
|
const svgTotp = `<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><rect x="5" y="2" width="14" height="20" rx="2" stroke="currentColor" stroke-width="1.7"/><circle cx="12" cy="17" r="1" fill="currentColor"/><path d="M9 7h6M9 11h4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`;
|
||||||
|
|
||||||
listEl.innerHTML = items.map(item => {
|
listEl.innerHTML = items.map(item => {
|
||||||
const matched = isMatch(item);
|
const matched = isMatch(item);
|
||||||
@@ -404,21 +470,44 @@ function renderList() {
|
|||||||
const emoji = itemEmoji(item.item_type);
|
const emoji = itemEmoji(item.item_type);
|
||||||
const canFill = item.item_type === 'password' && item.plain?.username && item.plain?.password;
|
const canFill = item.item_type === 'password' && item.plain?.username && item.plain?.password;
|
||||||
const canCopy = item.item_type === 'password' && item.plain?.password;
|
const canCopy = item.item_type === 'password' && item.plain?.password;
|
||||||
|
const hasTotp = item.item_type === 'password' && !!_extractTotpSecret(item.plain?.totp_uri);
|
||||||
|
|
||||||
return `<div class="vault-item" data-id="${item.id}">
|
return `<div class="vault-item" data-id="${item.id}">
|
||||||
<div class="item-avatar ${color}">${emoji}</div>
|
<div class="item-avatar ${color}">${emoji}</div>
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<div class="item-site">${site}${badge}</div>
|
<div class="item-site">${site}${badge}</div>
|
||||||
<div class="item-name">${name}</div>
|
<div class="item-name">${name}</div>
|
||||||
|
${hasTotp ? `<div class="item-totp-row"><span class="totp-code-inline" id="totp-${item.id}">······</span><span class="totp-timer-inline" id="totp-t-${item.id}"></span></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
${canCopy ? `<button class="btn-item-action" data-copy-pass="${item.id}" title="Copy password">${svgCopy}</button>` : ''}
|
${canCopy ? `<button class="btn-item-action" data-copy-pass="${item.id}" title="Copy password">${svgCopy}</button>` : ''}
|
||||||
|
${hasTotp ? `<button class="btn-item-action totp-btn" data-copy-totp="${item.id}" title="Copy 2FA code">${svgTotp}</button>` : ''}
|
||||||
${canFill ? `<button class="btn-item-action" data-autofill="${item.id}" title="Autofill">${svgFill}</button>` : ''}
|
${canFill ? `<button class="btn-item-action" data-autofill="${item.id}" title="Autofill">${svgFill}</button>` : ''}
|
||||||
<button class="btn-item-action" data-menu="${item.id}" title="More">${svgDots}</button>
|
<button class="btn-item-action" data-menu="${item.id}" title="More">${svgDots}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Start TOTP tickers for items that have a totp_uri.
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.item_type !== 'password' || !_extractTotpSecret(item.plain?.totp_uri)) return;
|
||||||
|
const codeEl = $(`totp-${item.id}`);
|
||||||
|
const timerEl = $(`totp-t-${item.id}`);
|
||||||
|
if (!codeEl) return;
|
||||||
|
|
||||||
|
async function tick() {
|
||||||
|
const code = await getTotpCode(item.plain.totp_uri).catch(() => null);
|
||||||
|
if (!code || !codeEl.isConnected) return;
|
||||||
|
codeEl.textContent = code.slice(0, 3) + ' ' + code.slice(3);
|
||||||
|
const secs = _totpSecondsLeft();
|
||||||
|
timerEl.textContent = ' ' + secs + 's';
|
||||||
|
timerEl.style.color = secs <= 5 ? '#dc2626' : '#9ca3af';
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
const id = setInterval(tick, 1000);
|
||||||
|
_totpIntervals.push(id);
|
||||||
|
});
|
||||||
|
|
||||||
// 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', e => {
|
||||||
@@ -432,6 +521,22 @@ function renderList() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Copy TOTP code
|
||||||
|
listEl.querySelectorAll('[data-copy-totp]').forEach(btn =>
|
||||||
|
btn.addEventListener('click', async e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const item = _items.find(i => i.id === parseInt(btn.dataset.copyTotp));
|
||||||
|
if (!item?.plain?.totp_uri) return;
|
||||||
|
const code = await getTotpCode(item.plain.totp_uri).catch(() => null);
|
||||||
|
if (code) {
|
||||||
|
navigator.clipboard.writeText(code);
|
||||||
|
btn.title = 'Copied!';
|
||||||
|
btn.style.color = '#16a34a';
|
||||||
|
setTimeout(() => { btn.title = 'Copy 2FA code'; btn.style.color = ''; }, 1500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Autofill
|
// Autofill
|
||||||
listEl.querySelectorAll('[data-autofill]').forEach(btn =>
|
listEl.querySelectorAll('[data-autofill]').forEach(btn =>
|
||||||
btn.addEventListener('click', async e => {
|
btn.addEventListener('click', async e => {
|
||||||
@@ -456,7 +561,6 @@ function renderList() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const item = _items.find(i => i.id === parseInt(btn.dataset.menu));
|
const item = _items.find(i => i.id === parseInt(btn.dataset.menu));
|
||||||
if (!item?.plain) return;
|
if (!item?.plain) return;
|
||||||
// Simple: copy username on dots click (could be a dropdown in future)
|
|
||||||
if (item.plain.username) {
|
if (item.plain.username) {
|
||||||
navigator.clipboard.writeText(item.plain.username);
|
navigator.clipboard.writeText(item.plain.username);
|
||||||
btn.title = 'Username copied!';
|
btn.title = 'Username copied!';
|
||||||
|
|||||||
Reference in New Issue
Block a user