04/21 Fix bugs

This commit is contained in:
2026-04-22 22:10:46 -04:00
parent 0aa2432942
commit f507dee19f
5 changed files with 1102 additions and 659 deletions
+77 -2
View File
@@ -170,7 +170,6 @@ ul {
/* Prevent iOS viewport auto-zoom on input focus (requires font-size >= 16px) */ /* Prevent iOS viewport auto-zoom on input focus (requires font-size >= 16px) */
@media (max-width: 768px) { @media (max-width: 768px) {
.form-group input, .form-group input,
.form-group select, .form-group select,
.form-group textarea { .form-group textarea {
@@ -1139,4 +1138,80 @@ body.is-mobile .form-group textarea {
color: var(--color-danger, #e53e3e); color: var(--color-danger, #e53e3e);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
} }
/* ── CSP-safe replacements for former inline styles ─────────────────── */
/* TOTP timer: turns red when ≤5 s remain (toggled via JS class) */
.totp-timer--expiring {
color: #dc2626;
}
/* Generate-password button injected below password field */
.btn-generate {
margin-top: 6px;
}
/* Scroll-lock applied to <html> when mobile sidebar is open */
html.sidebar-open {
overflow: hidden;
}
/* Password Generator modal max-width */
.gen-modal-inner {
max-width: 420px;
}
/* Generator length slider */
#gen-length {
width: 100%;
margin-top: 4px;
}
/* Generator options row */
.gen-options {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
/* Generator form-group spacing */
.gen-form-group {
margin-bottom: 8px;
}
/* Generator input-with-toggle wrapper */
.gen-input-wrap {
margin-bottom: 4px;
}
/* Generator copy button */
#btn-gen-copy {
font-size: 14px;
}
/* Generator output field */
#gen-output {
padding-right: 40px;
}
/* Form field label — "optional" badge */
.field-label-optional {
font-size: 11px;
color: #9ca3af;
font-weight: 400;
}
/* Form field hint text below inputs */
.field-hint {
font-size: 11px;
color: #6b7280;
margin-top: 4px;
}
/* Monospace input — used for recovery codes and other token fields */
.input-monospace {
font-family: monospace;
letter-spacing: 0.05em;
}
+516 -494
View File
File diff suppressed because it is too large Load Diff
+62 -24
View File
@@ -1,8 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Recover Account — PassKeeper{%
{% block title %}Recover Account — PassKeeper{% endblock %} endblock %} {% block body_class %}auth-page{% endblock %} {% block body %}
{% block body_class %}auth-page{% endblock %}
{% block body %}
<div class="auth-container"> <div class="auth-container">
<div class="auth-card"> <div class="auth-card">
<div class="auth-logo"> <div class="auth-logo">
@@ -13,21 +10,40 @@
<!-- Step 1: Email + Recovery Code --> <!-- Step 1: Email + Recovery Code -->
<div id="recover-step-1"> <div id="recover-step-1">
<h1 class="auth-title">Recover your account</h1> <h1 class="auth-title">Recover your account</h1>
<p class="auth-step-hint">Enter your email and the recovery code you saved when you set up account recovery.</p> <p class="auth-step-hint">
Enter your email and the recovery code you saved when you set up account
recovery.
</p>
<form id="recover-form-step1" novalidate> <form id="recover-form-step1" novalidate>
<p id="recover-error-1" class="form-error hidden"></p> <p id="recover-error-1" class="form-error hidden"></p>
<div class="form-group"> <div class="form-group">
<label for="recover-email">Email address</label> <label for="recover-email">Email address</label>
<input type="email" id="recover-email" name="email" required <input
autocomplete="email" placeholder="you@example.com"> type="email"
id="recover-email"
name="email"
required
autocomplete="email"
placeholder="you@example.com"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="recover-code">Recovery code</label> <label for="recover-code">Recovery code</label>
<input type="text" id="recover-code" name="recovery_code" required <input
autocomplete="off" placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx" type="text"
style="font-family: monospace; letter-spacing: 0.05em;"> id="recover-code"
name="recovery_code"
required
autocomplete="off"
placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"
class="input-monospace"
/>
</div> </div>
<button type="submit" class="btn-primary btn-full" data-loading-text="Verifying…"> <button
type="submit"
class="btn-primary btn-full"
data-loading-text="Verifying…"
>
Verify recovery code Verify recovery code
</button> </button>
</form> </form>
@@ -39,34 +55,56 @@
<!-- Step 2: Set new master password (hidden until step 1 verified) --> <!-- Step 2: Set new master password (hidden until step 1 verified) -->
<div id="recover-step-2" class="hidden"> <div id="recover-step-2" class="hidden">
<h1 class="auth-title">Set a new master password</h1> <h1 class="auth-title">Set a new master password</h1>
<p class="auth-step-hint">Your recovery code is valid. Choose a new master password — your vault will be re-encrypted automatically.</p> <p class="auth-step-hint">
Your recovery code is valid. Choose a new master password — your vault
will be re-encrypted automatically.
</p>
<form id="recover-form-step2" novalidate> <form id="recover-form-step2" novalidate>
<p id="recover-error-2" class="form-error hidden"></p> <p id="recover-error-2" class="form-error hidden"></p>
<div class="form-group"> <div class="form-group">
<label for="recover-new-pass">New master password</label> <label for="recover-new-pass">New master password</label>
<div class="input-with-toggle"> <div class="input-with-toggle">
<input type="password" id="recover-new-pass" name="new_password" required <input
autocomplete="new-password" placeholder="Minimum 12 characters"> type="password"
<button type="button" class="btn-show-pass" id="toggle-recover-pass" id="recover-new-pass"
aria-label="Toggle password visibility">👁</button> name="new_password"
required
autocomplete="new-password"
placeholder="Minimum 12 characters"
/>
<button
type="button"
class="btn-show-pass"
id="toggle-recover-pass"
aria-label="Toggle password visibility"
>
👁
</button>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="recover-confirm-pass">Confirm new password</label> <label for="recover-confirm-pass">Confirm new password</label>
<input type="password" id="recover-confirm-pass" name="confirm_password" required <input
autocomplete="new-password" placeholder="Repeat new password"> type="password"
id="recover-confirm-pass"
name="confirm_password"
required
autocomplete="new-password"
placeholder="Repeat new password"
/>
</div> </div>
<button type="submit" class="btn-primary btn-full" data-loading-text="Recovering account…"> <button
type="submit"
class="btn-primary btn-full"
data-loading-text="Recovering account…"
>
Recover account Recover account
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %} {% block scripts %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/crypto.js') }}"></script> <script src="{{ url_for('static', filename='js/crypto.js') }}"></script>
<script src="{{ url_for('static', filename='js/recover.js') }}"></script> <script src="{{ url_for('static', filename='js/recover.js') }}"></script>
{% endblock %} {% endblock %}
+359 -88
View File
@@ -7,7 +7,11 @@
</button> </button>
<span class="mobile-logo">🔒 PassKeeper</span> <span class="mobile-logo">🔒 PassKeeper</span>
<button class="btn-fab btn-fab-mobile" id="btn-add-item-mobile" title="Add item"> <button
class="btn-fab btn-fab-mobile"
id="btn-add-item-mobile"
title="Add item"
>
+ +
</button> </button>
</div> </div>
@@ -20,56 +24,109 @@
<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 class="btn-sidebar-toggle" id="btn-sidebar-toggle" aria-label="Collapse sidebar" title="Collapse sidebar"> <button
class="btn-sidebar-toggle"
id="btn-sidebar-toggle"
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
&#x276E; &#x276E;
</button> </button>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<ul> <ul>
<li class="sidebar-item active" id="sidebar-all" data-view="vault" data-tooltip="All Items"> <li
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 class="sidebar-item" data-type-filter="password" data-tooltip="Passwords"> <li
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 class="sidebar-item" data-type-filter="note" data-tooltip="Secure Notes"> <li
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 class="sidebar-item" data-type-filter="card" data-tooltip="Payment Cards"> <li
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 class="sidebar-item" data-type-filter="bank" data-tooltip="Bank Accounts"> <li
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 class="sidebar-item" data-type-filter="address" data-tooltip="Addresses"> <li
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 class="sidebar-item" data-type-filter="ssn" data-tooltip="Identities"> <li
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 class="sidebar-item" id="sidebar-security" data-view="security" data-tooltip="Security"> <li
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 class="sidebar-item" id="sidebar-sharing" data-view="sharing" data-tooltip="Sharing"> <li
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 class="sidebar-item" id="sidebar-emergency" data-view="emergency" data-tooltip="Emergency Access"> <li
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 class="sidebar-item" id="sidebar-generator" data-tooltip="Password Generator"> <li
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>
@@ -81,9 +138,19 @@
</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 type="text" id="new-folder-input" placeholder="Folder name" maxlength="128" /> <input
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 type="button" class="btn-icon" id="btn-cancel-folder" title="Cancel"> <button
type="button"
class="btn-icon"
id="btn-cancel-folder"
title="Cancel"
>
</button> </button>
</form> </form>
@@ -93,11 +160,19 @@
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<button class="sidebar-item btn-text" id="btn-settings" data-tooltip="Settings"> <button
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 class="sidebar-item btn-text" id="btn-logout" data-tooltip="Sign out"> <button
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>
@@ -114,7 +189,12 @@
<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 type="search" id="search-input" placeholder="Search vault…" autocomplete="off" /> <input
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">
@@ -224,7 +304,13 @@
</div> </div>
<!-- ── Unlock overlay ──────────────────────────────────────────────── --> <!-- ── Unlock overlay ──────────────────────────────────────────────── -->
<div id="unlock-overlay" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="unlock-title"> <div
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>
@@ -237,8 +323,12 @@
<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 type="password" id="unlock-password" autocomplete="current-password" <input
placeholder="Enter master password" /> type="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>
@@ -249,11 +339,21 @@
</div> </div>
<!-- ── Add / Edit Item Modal ─────────────────────────────────────────── --> <!-- ── Add / Edit Item Modal ─────────────────────────────────────────── -->
<div id="item-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <div
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 class="btn-icon btn-close" id="btn-close-modal" aria-label="Close"> <button
class="btn-icon btn-close"
id="btn-close-modal"
aria-label="Close"
>
</button> </button>
</div> </div>
@@ -271,49 +371,92 @@
</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 type="text" id="field-name" name="name" required placeholder="e.g. Gmail" /> <input
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><input type="url" id="field-url" <label for="field-url">Website URL</label
placeholder="https://example.com" /> ><input type="url" id="field-url" placeholder="https://example.com" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-username">Username / Email</label><input type="text" id="field-username" autocomplete="off" <label for="field-username">Username / Email</label
placeholder="username or email" /> ><input
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 type="password" id="field-password" autocomplete="new-password" placeholder="Enter password" /> <input
<button type="button" class="btn-show-pass" id="btn-toggle-pass" aria-label="Toggle"> type="password"
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"> <div class="form-group">
<label for="field-totp-uri">Authenticator Key (TOTP) <span <label for="field-totp-uri"
style="font-size:11px;color:#9ca3af;font-weight:400;">optional</span></label> >Authenticator Key (TOTP)
<input type="text" id="field-totp-uri" autocomplete="off" <span class="field-label-optional">optional</span></label
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 <input
plain base32 secret key. PassKeeper will generate your 2FA codes.</p> type="text"
id="field-totp-uri"
autocomplete="off"
placeholder="otpauth://totp/… or plain base32 secret"
/>
<p class="field-hint">
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>
<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><textarea id="field-note-body" rows="6" <label for="field-note-body">Note</label
placeholder="Enter your secure note…"></textarea> ><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><input type="text" id="field-cardholder" <label for="field-cardholder">Cardholder Name</label
placeholder="Name on card" /> ><input
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><input type="text" id="field-card-number" autocomplete="off" <label for="field-card-number">Card Number</label
placeholder="•••• •••• •••• ••••" maxlength="19" /> ><input
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">
@@ -335,19 +478,30 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-expiry-year">Year</label><input type="text" id="field-expiry-year" placeholder="YYYY" <label for="field-expiry-year">Year</label
maxlength="4" /> ><input
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><input type="text" id="field-cvv" autocomplete="off" placeholder="•••" <label for="field-cvv">CVV</label
maxlength="4" /> ><input
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><input type="text" id="field-bank-name" <label for="field-bank-name">Bank Name</label
placeholder="e.g. Chase" /> ><input type="text" id="field-bank-name" 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>
@@ -360,56 +514,88 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-routing">Routing Number</label><input type="text" id="field-routing" autocomplete="off" <label for="field-routing">Routing Number</label
placeholder="9-digit routing number" maxlength="9" /> ><input
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><input type="text" id="field-account-number" <label for="field-account-number">Account Number</label
autocomplete="off" placeholder="Account number" /> ><input
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><input type="text" id="field-first-name" <label for="field-first-name">First Name</label
placeholder="First name" /> ><input
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><input type="text" id="field-last-name" <label for="field-last-name">Last Name</label
placeholder="Last name" /> ><input type="text" id="field-last-name" placeholder="Last name" />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-company">Company</label><input type="text" id="field-company" <label for="field-company">Company</label
placeholder="Company (optional)" /> ><input
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><input type="text" id="field-address-line" <label for="field-address-line">Street Address</label
placeholder="123 Main St" /> ><input
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><input type="text" id="field-city" placeholder="City" /> <label for="field-city">City</label
><input type="text" id="field-city" placeholder="City" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-state">State</label><input type="text" id="field-state" placeholder="State" /> <label for="field-state">State</label
><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><input type="text" id="field-zip" placeholder="ZIP" /> <label for="field-zip">ZIP</label
><input type="text" id="field-zip" placeholder="ZIP" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="field-country">Country</label><input type="text" id="field-country" placeholder="Country" /> <label for="field-country">Country</label
><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><input type="tel" id="field-phone" placeholder="+1 555-000-0000" /> <label for="field-phone">Phone</label
><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><input type="email" id="field-email" <label for="field-email">Email</label
placeholder="email@example.com" /> ><input
type="email"
id="field-email"
placeholder="email@example.com"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -417,8 +603,19 @@
<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 type="password" id="field-ssn-number" autocomplete="off" placeholder="•••-••-••••" maxlength="11" /> <input
<button type="button" class="btn-show-pass" id="btn-toggle-ssn" aria-label="Toggle"> type="password"
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>
@@ -432,8 +629,12 @@
</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><textarea id="field-notes" rows="3" <label for="field-notes">Notes</label
placeholder="Optional notes…"></textarea> ><textarea
id="field-notes"
rows="3"
placeholder="Optional notes…"
></textarea>
</div> </div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
@@ -451,7 +652,11 @@
<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 class="btn-icon btn-close" id="btn-close-settings" aria-label="Close"> <button
class="btn-icon btn-close"
id="btn-close-settings"
aria-label="Close"
>
</button> </button>
</div> </div>
@@ -472,7 +677,13 @@
</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 type="text" id="mfa-verify-code" inputmode="numeric" maxlength="6" placeholder="000000" /> <input
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">
@@ -481,8 +692,16 @@
</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">Enter your current code to disable MFA</label> <label for="mfa-disable-code"
<input type="text" id="mfa-disable-code" inputmode="numeric" maxlength="6" placeholder="000000" /> >Enter your current code to disable MFA</label
>
<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
@@ -513,15 +732,30 @@
<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 type="password" id="cp-current" autocomplete="current-password" placeholder="Current password" /> <input
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 type="password" id="cp-new" autocomplete="new-password" placeholder="Minimum 12 characters" /> <input
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 type="password" id="cp-confirm" autocomplete="new-password" placeholder="Repeat new password" /> <input
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">
@@ -542,7 +776,11 @@
<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 class="btn-icon" id="btn-copy-recovery-code" title="Copy to clipboard"> <button
class="btn-icon"
id="btn-copy-recovery-code"
title="Copy to clipboard"
>
📋 📋
</button> </button>
</div> </div>
@@ -566,8 +804,15 @@
<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">Enter your master password to confirm</label> <label for="delete-password"
<input type="password" id="delete-password" autocomplete="current-password" placeholder="Master password" /> >Enter your master password to confirm</label
>
<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">
@@ -590,7 +835,11 @@
<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 class="btn-icon btn-close" id="btn-close-share-modal" aria-label="Close"> <button
class="btn-icon btn-close"
id="btn-close-share-modal"
aria-label="Close"
>
</button> </button>
</div> </div>
@@ -602,7 +851,11 @@
</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 type="email" id="share-recipient-email" placeholder="recipient@example.com" /> <input
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">
@@ -619,7 +872,11 @@
<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 class="btn-icon btn-close" id="btn-close-emergency-modal" aria-label="Close"> <button
class="btn-icon btn-close"
id="btn-close-emergency-modal"
aria-label="Close"
>
</button> </button>
</div> </div>
@@ -631,7 +888,11 @@
</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 type="email" id="emergency-email" placeholder="contact@example.com" /> <input
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>
@@ -654,11 +915,21 @@
</div> </div>
<!-- ── Item Detail Modal (read-only: shared items) ────────────────────── --> <!-- ── Item Detail Modal (read-only: shared items) ────────────────────── -->
<div id="detail-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="detail-modal-title"> <div
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 class="btn-icon btn-close" id="btn-close-detail-modal" aria-label="Close"> <button
class="btn-icon btn-close"
id="btn-close-detail-modal"
aria-label="Close"
>
</button> </button>
</div> </div>
@@ -676,4 +947,4 @@
<script src="{{ url_for('static', filename='js/auth.js') }}"></script> <script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script src="{{ url_for('static', filename='js/sharing.js') }}"></script> <script src="{{ url_for('static', filename='js/sharing.js') }}"></script>
<script src="{{ url_for('static', filename='js/vault.js') }}"></script> <script src="{{ url_for('static', filename='js/vault.js') }}"></script>
{% endblock %} {% endblock %}
+88 -51
View File
@@ -12,7 +12,7 @@
// Default: 10 minutes. User can change via the Account view in the popup. // Default: 10 minutes. User can change via the Account view in the popup.
const DEFAULT_IDLE_LOCK_SECONDS = 600; const DEFAULT_IDLE_LOCK_SECONDS = 600;
const IDLE_TIMEOUT_KEY = 'idle_lock_seconds'; const IDLE_TIMEOUT_KEY = "idle_lock_seconds";
/** Apply the idle detection interval, reading the user's saved preference. */ /** Apply the idle detection interval, reading the user's saved preference. */
async function applyIdleInterval() { async function applyIdleInterval() {
@@ -24,24 +24,27 @@ async function applyIdleInterval() {
} else { } else {
chrome.idle.setDetectionInterval(Math.max(15, seconds)); chrome.idle.setDetectionInterval(Math.max(15, seconds));
} }
console.log('[PassKeeper] Idle lock interval:', seconds === 0 ? 'never' : seconds + 's'); console.log(
"[PassKeeper] Idle lock interval:",
seconds === 0 ? "never" : seconds + "s",
);
} }
// Apply on every SW startup (SW can be killed and restarted at any time). // Apply on every SW startup (SW can be killed and restarted at any time).
applyIdleInterval(); applyIdleInterval();
chrome.idle.onStateChanged.addListener(async (newState) => { chrome.idle.onStateChanged.addListener(async (newState) => {
if (newState === 'idle' || newState === 'locked') { if (newState === "idle" || newState === "locked") {
// Check if the user set "Never" before locking. // Check if the user set "Never" before locking.
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY); const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return; if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return;
console.log('[PassKeeper] System', newState, '— locking vault.'); console.log("[PassKeeper] System", newState, "— locking vault.");
await chrome.storage.session.clear(); await chrome.storage.session.clear();
await chrome.storage.local.remove('vault_items_cs'); await chrome.storage.local.remove("vault_items_cs");
const tabs = await chrome.tabs.query({}); const tabs = await chrome.tabs.query({});
tabs.forEach(tab => { tabs.forEach((tab) => {
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id }); if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id });
}); });
} }
}); });
@@ -50,37 +53,49 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
async function updateBadgeForTab(tabId, url) { async function updateBadgeForTab(tabId, url) {
try { try {
const { vault_items } = await chrome.storage.session.get('vault_items'); const { vault_items } = await chrome.storage.session.get("vault_items");
if (!vault_items?.length) { if (!vault_items?.length) {
chrome.action.setBadgeText({ text: '', tabId }); chrome.action.setBadgeText({ text: "", tabId });
return; return;
} }
let hostname; let hostname;
try { hostname = new URL(url).hostname.replace(/^www\./, ''); } try {
catch { chrome.action.setBadgeText({ text: '', tabId }); return; } hostname = new URL(url).hostname.replace(/^www\./, "");
} catch {
chrome.action.setBadgeText({ text: "", tabId });
return;
}
const matches = vault_items.filter(item => { const matches = vault_items.filter((item) => {
if (item.item_type !== 'password' || !item.plain?.url) return false; if (item.item_type !== "password" || !item.plain?.url) return false;
try { try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, ''); const h = new URL(item.plain.url).hostname.replace(/^www\./, "");
return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`); return (
} catch { return false; } h === hostname ||
h.endsWith(`.${hostname}`) ||
hostname.endsWith(`.${h}`)
);
} catch {
return false;
}
}); });
if (matches.length > 0) { if (matches.length > 0) {
chrome.action.setBadgeText({ text: String(matches.length), tabId }); chrome.action.setBadgeText({ text: String(matches.length), tabId });
chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId }); chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
} else { } else {
chrome.action.setBadgeText({ text: '', tabId }); chrome.action.setBadgeText({ text: "", tabId });
} }
} catch { /* tab may have closed */ } } catch {
/* tab may have closed */
}
} }
async function refreshAllBadges() { async function refreshAllBadges() {
const tabs = await chrome.tabs.query({}); const tabs = await chrome.tabs.query({});
for (const tab of tabs) { for (const tab of tabs) {
if (tab.url?.startsWith('http')) updateBadgeForTab(tab.id, tab.url); if (tab.url?.startsWith("http")) updateBadgeForTab(tab.id, tab.url);
} }
} }
@@ -89,12 +104,12 @@ async function refreshAllBadges() {
chrome.tabs.onActivated.addListener(async ({ tabId }) => { chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try { try {
const tab = await chrome.tabs.get(tabId); const tab = await chrome.tabs.get(tabId);
if (tab.url?.startsWith('http')) updateBadgeForTab(tabId, tab.url); if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url);
} catch { } } catch {}
}); });
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url?.startsWith('http')) { if (changeInfo.status === "complete" && tab.url?.startsWith("http")) {
updateBadgeForTab(tabId, tab.url); updateBadgeForTab(tabId, tab.url);
} }
}); });
@@ -102,24 +117,30 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// ── Message handler ────────────────────────────────────────────────────────── // ── Message handler ──────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Content script detected a login form on the page — no action needed in the
// background, but we must respond to close the message port cleanly.
if (msg.type === "FORMS_DETECTED") {
sendResponse({ ok: true });
return false;
}
// Popup updated the idle lock timeout → re-apply immediately. // Popup updated the idle lock timeout → re-apply immediately.
if (msg.type === 'SET_IDLE_TIMEOUT') { if (msg.type === "SET_IDLE_TIMEOUT") {
applyIdleInterval(); applyIdleInterval();
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Content script requests opening the vault tab (from suggestion dropdown). // Content script requests opening the vault tab (from suggestion dropdown).
if (msg.type === 'OPEN_VAULT') { if (msg.type === "OPEN_VAULT") {
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' }); chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" });
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Content script requests navigating the popup to the generator view. // Content script requests navigating the popup to the generator view.
if (msg.type === 'OPEN_GENERATOR') { if (msg.type === "OPEN_GENERATOR") {
chrome.storage.session.set({ popup_nav: 'generator' }); chrome.storage.session.set({ popup_nav: "generator" });
chrome.action.openPopup().catch(() => { chrome.action.openPopup().catch(() => {
// openPopup() requires user gesture in some Chrome versions — fallback is a no-op. // openPopup() requires user gesture in some Chrome versions — fallback is a no-op.
}); });
@@ -129,20 +150,26 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// No-op ping from popup — keeps the service worker alive while the browser // No-op ping from popup — keeps the service worker alive while the browser
// is open so chrome.storage.session is not wiped between popup openings. // is open so chrome.storage.session is not wiped between popup openings.
if (msg.type === 'KEEPALIVE') { if (msg.type === "KEEPALIVE") {
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Popup signals vault cache was refreshed → re-check badges AND forward // Popup signals vault cache was refreshed → re-check badges AND forward
// the decrypted items directly to all content scripts (avoids storage read). // the decrypted items directly to all content scripts (avoids storage read).
if (msg.type === 'VAULT_UPDATED') { if (msg.type === "VAULT_UPDATED") {
refreshAllBadges(); refreshAllBadges();
const payload = { type: 'VAULT_UPDATED', vault_items: msg.vault_items || [] }; const payload = {
type: "VAULT_UPDATED",
vault_items: msg.vault_items || [],
};
chrome.tabs.query({}, function (tabs) { chrome.tabs.query({}, function (tabs) {
tabs.forEach(function (tab) { tabs.forEach(function (tab) {
if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) { if (
chrome.tabs.sendMessage(tab.id, payload).catch(function () { }); tab.url &&
(tab.url.startsWith("http://") || tab.url.startsWith("https://"))
) {
chrome.tabs.sendMessage(tab.id, payload).catch(function () {});
} }
}); });
}); });
@@ -151,7 +178,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Content script detected credentials on form submit → store for save-prompt. // Content script detected credentials on form submit → store for save-prompt.
if (msg.type === 'SAVE_CREDENTIALS') { if (msg.type === "SAVE_CREDENTIALS") {
// Store in local storage so the prompt survives service worker restarts // Store in local storage so the prompt survives service worker restarts
// and is guaranteed to be present when the user next opens the popup. // and is guaranteed to be present when the user next opens the popup.
chrome.storage.local.set({ pending_save: msg.data }); chrome.storage.local.set({ pending_save: msg.data });
@@ -160,10 +187,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Bridge: web app logged in → store tokens in extension storage. // Bridge: web app logged in → store tokens in extension storage.
if (msg.type === 'WEB_SESSION_SYNC') { if (msg.type === "WEB_SESSION_SYNC") {
chrome.storage.session.set({ chrome.storage.session.set({
access_token: msg.access_token, access_token: msg.access_token,
enc_key_salt: msg.enc_key_salt || '', enc_key_salt: msg.enc_key_salt || "",
}); });
// Also persist enc_key_salt locally so the unlock-only view survives browser restarts // Also persist enc_key_salt locally so the unlock-only view survives browser restarts
chrome.storage.local.set({ chrome.storage.local.set({
@@ -175,28 +202,38 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Bridge: web app logged out → clear extension session. // Bridge: web app logged out → clear extension session.
if (msg.type === 'WEB_SESSION_CLEAR') { if (msg.type === "WEB_SESSION_CLEAR") {
chrome.storage.session.remove(['access_token', 'vault_key_jwk', 'vault_items', 'enc_key_salt']); chrome.storage.session.remove([
chrome.storage.local.remove('refresh_token'); "access_token",
"vault_key_jwk",
"vault_items",
"enc_key_salt",
]);
chrome.storage.local.remove("refresh_token");
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Popup logged in via extension → inject session into open vault tabs. // Popup logged in via extension → inject session into open vault tabs.
if (msg.type === 'EXT_SESSION_SYNC') { if (msg.type === "EXT_SESSION_SYNC") {
chrome.tabs.query({ url: 'https://pwkeeper.ngodanguyen.tech/*' }, tabs => { chrome.tabs.query(
tabs.forEach(tab => { { url: "https://pwkeeper.ngodanguyen.tech/*" },
chrome.tabs.sendMessage(tab.id, { (tabs) => {
type: 'INJECT_SESSION', tabs.forEach((tab) => {
access_token: msg.access_token, chrome.tabs
refresh_token: msg.refresh_token, .sendMessage(tab.id, {
enc_key_salt: msg.enc_key_salt, type: "INJECT_SESSION",
}).catch(() => { }); access_token: msg.access_token,
}); refresh_token: msg.refresh_token,
}); enc_key_salt: msg.enc_key_salt,
})
.catch(() => {});
});
},
);
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
return false; return false;
}); });