Merge branch 'main' of https://gitea.ngodanguyen.tech/nngo/PassKeeper
This commit is contained in:
+77
-2
@@ -170,7 +170,6 @@ ul {
|
||||
|
||||
/* Prevent iOS viewport auto-zoom on input focus (requires font-size >= 16px) */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
@@ -1956,4 +1955,80 @@ body.is-mobile .form-group textarea {
|
||||
color: var(--color-danger, #e53e3e);
|
||||
font-size: 13px;
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -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 %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
@@ -13,21 +10,40 @@
|
||||
<!-- Step 1: Email + Recovery Code -->
|
||||
<div id="recover-step-1">
|
||||
<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>
|
||||
<p id="recover-error-1" class="form-error hidden"></p>
|
||||
<div class="form-group">
|
||||
<label for="recover-email">Email address</label>
|
||||
<input type="email" id="recover-email" name="email" required
|
||||
autocomplete="email" placeholder="you@example.com">
|
||||
<input
|
||||
type="email"
|
||||
id="recover-email"
|
||||
name="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recover-code">Recovery code</label>
|
||||
<input type="text" id="recover-code" name="recovery_code" required
|
||||
autocomplete="off" placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"
|
||||
style="font-family: monospace; letter-spacing: 0.05em;">
|
||||
<input
|
||||
type="text"
|
||||
id="recover-code"
|
||||
name="recovery_code"
|
||||
required
|
||||
autocomplete="off"
|
||||
placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"
|
||||
class="input-monospace"
|
||||
/>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
@@ -39,34 +55,56 @@
|
||||
<!-- Step 2: Set new master password (hidden until step 1 verified) -->
|
||||
<div id="recover-step-2" class="hidden">
|
||||
<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>
|
||||
<p id="recover-error-2" class="form-error hidden"></p>
|
||||
<div class="form-group">
|
||||
<label for="recover-new-pass">New master password</label>
|
||||
<div class="input-with-toggle">
|
||||
<input type="password" id="recover-new-pass" 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>
|
||||
<input
|
||||
type="password"
|
||||
id="recover-new-pass"
|
||||
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 class="form-group">
|
||||
<label for="recover-confirm-pass">Confirm new password</label>
|
||||
<input type="password" id="recover-confirm-pass" name="confirm_password" required
|
||||
autocomplete="new-password" placeholder="Repeat new password">
|
||||
<input
|
||||
type="password"
|
||||
id="recover-confirm-pass"
|
||||
name="confirm_password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="Repeat new password"
|
||||
/>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/crypto.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/recover.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
+359
-88
@@ -7,7 +7,11 @@
|
||||
☰
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
@@ -20,56 +24,109 @@
|
||||
<div class="sidebar-header">
|
||||
<span class="logo-icon">🔒</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"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<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-label">All Items</span>
|
||||
</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-label">Passwords</span>
|
||||
</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-label">Secure Notes</span>
|
||||
</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-label">Payment Cards</span>
|
||||
</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-label">Bank Accounts</span>
|
||||
</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-label">Addresses</span>
|
||||
</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-label">Identities</span>
|
||||
</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-label">Security</span>
|
||||
</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-label">Sharing</span>
|
||||
</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-label">Emergency Access</span>
|
||||
</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-label">Password Generator</span>
|
||||
</li>
|
||||
@@ -81,9 +138,19 @@
|
||||
</li>
|
||||
<li id="new-folder-row" class="new-folder-row hidden">
|
||||
<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="button" class="btn-icon" id="btn-cancel-folder" title="Cancel">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
id="btn-cancel-folder"
|
||||
title="Cancel"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
@@ -93,11 +160,19 @@
|
||||
</nav>
|
||||
|
||||
<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-label">Settings</span>
|
||||
</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-label">Sign out</span>
|
||||
</button>
|
||||
@@ -114,7 +189,12 @@
|
||||
<div class="vault-toolbar">
|
||||
<div class="search-wrapper">
|
||||
<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 class="sort-wrapper">
|
||||
<select id="sort-select" title="Sort by">
|
||||
@@ -224,7 +304,13 @@
|
||||
</div>
|
||||
|
||||
<!-- ── 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="auth-logo unlock-logo">
|
||||
<span class="logo-icon">🔒</span><span class="logo-text">PassKeeper</span>
|
||||
@@ -237,8 +323,12 @@
|
||||
<form id="unlock-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="unlock-password">Master password</label>
|
||||
<input type="password" id="unlock-password" autocomplete="current-password"
|
||||
placeholder="Enter master password" />
|
||||
<input
|
||||
type="password"
|
||||
id="unlock-password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Enter master password"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<a href="/login" class="btn-secondary btn-link">Sign out</a>
|
||||
@@ -249,11 +339,21 @@
|
||||
</div>
|
||||
|
||||
<!-- ── 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-header">
|
||||
<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>
|
||||
</div>
|
||||
@@ -271,49 +371,92 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 class="type-fields" data-for-types="password">
|
||||
<div class="form-group">
|
||||
<label for="field-url">Website URL</label><input type="url" id="field-url"
|
||||
placeholder="https://example.com" />
|
||||
<label for="field-url">Website URL</label
|
||||
><input type="url" id="field-url" placeholder="https://example.com" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-username">Username / Email</label><input type="text" id="field-username" autocomplete="off"
|
||||
placeholder="username or email" />
|
||||
<label for="field-username">Username / Email</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-username"
|
||||
autocomplete="off"
|
||||
placeholder="username or email"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-password">Password</label>
|
||||
<div class="input-with-toggle">
|
||||
<input 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">
|
||||
<input
|
||||
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>
|
||||
</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>
|
||||
<label for="field-totp-uri"
|
||||
>Authenticator Key (TOTP)
|
||||
<span class="field-label-optional">optional</span></label
|
||||
>
|
||||
<input
|
||||
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 class="type-fields hidden" data-for-types="note">
|
||||
<div class="form-group">
|
||||
<label for="field-note-body">Note</label><textarea id="field-note-body" rows="6"
|
||||
placeholder="Enter your secure note…"></textarea>
|
||||
<label for="field-note-body">Note</label
|
||||
><textarea
|
||||
id="field-note-body"
|
||||
rows="6"
|
||||
placeholder="Enter your secure note…"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-fields hidden" data-for-types="card">
|
||||
<div class="form-group">
|
||||
<label for="field-cardholder">Cardholder Name</label><input type="text" id="field-cardholder"
|
||||
placeholder="Name on card" />
|
||||
<label for="field-cardholder">Cardholder Name</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-cardholder"
|
||||
placeholder="Name on card"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-card-number">Card Number</label><input type="text" id="field-card-number" autocomplete="off"
|
||||
placeholder="•••• •••• •••• ••••" maxlength="19" />
|
||||
<label for="field-card-number">Card Number</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-card-number"
|
||||
autocomplete="off"
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
maxlength="19"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@@ -335,19 +478,30 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-expiry-year">Year</label><input type="text" id="field-expiry-year" placeholder="YYYY"
|
||||
maxlength="4" />
|
||||
<label for="field-expiry-year">Year</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-expiry-year"
|
||||
placeholder="YYYY"
|
||||
maxlength="4"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-cvv">CVV</label><input type="text" id="field-cvv" autocomplete="off" placeholder="•••"
|
||||
maxlength="4" />
|
||||
<label for="field-cvv">CVV</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-cvv"
|
||||
autocomplete="off"
|
||||
placeholder="•••"
|
||||
maxlength="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-fields hidden" data-for-types="bank">
|
||||
<div class="form-group">
|
||||
<label for="field-bank-name">Bank Name</label><input type="text" id="field-bank-name"
|
||||
placeholder="e.g. Chase" />
|
||||
<label for="field-bank-name">Bank Name</label
|
||||
><input type="text" id="field-bank-name" placeholder="e.g. Chase" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-account-type">Account Type</label>
|
||||
@@ -360,56 +514,88 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-routing">Routing Number</label><input type="text" id="field-routing" autocomplete="off"
|
||||
placeholder="9-digit routing number" maxlength="9" />
|
||||
<label for="field-routing">Routing Number</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-routing"
|
||||
autocomplete="off"
|
||||
placeholder="9-digit routing number"
|
||||
maxlength="9"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-account-number">Account Number</label><input type="text" id="field-account-number"
|
||||
autocomplete="off" placeholder="Account number" />
|
||||
<label for="field-account-number">Account Number</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-account-number"
|
||||
autocomplete="off"
|
||||
placeholder="Account number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-fields hidden" data-for-types="address">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="field-first-name">First Name</label><input type="text" id="field-first-name"
|
||||
placeholder="First name" />
|
||||
<label for="field-first-name">First Name</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-first-name"
|
||||
placeholder="First name"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-last-name">Last Name</label><input type="text" id="field-last-name"
|
||||
placeholder="Last name" />
|
||||
<label for="field-last-name">Last Name</label
|
||||
><input type="text" id="field-last-name" placeholder="Last name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-company">Company</label><input type="text" id="field-company"
|
||||
placeholder="Company (optional)" />
|
||||
<label for="field-company">Company</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-company"
|
||||
placeholder="Company (optional)"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field-address-line">Street Address</label><input type="text" id="field-address-line"
|
||||
placeholder="123 Main St" />
|
||||
<label for="field-address-line">Street Address</label
|
||||
><input
|
||||
type="text"
|
||||
id="field-address-line"
|
||||
placeholder="123 Main St"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<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 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 class="form-row">
|
||||
<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 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 class="form-row">
|
||||
<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 class="form-group">
|
||||
<label for="field-email">Email</label><input type="email" id="field-email"
|
||||
placeholder="email@example.com" />
|
||||
<label for="field-email">Email</label
|
||||
><input
|
||||
type="email"
|
||||
id="field-email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,8 +603,19 @@
|
||||
<div class="form-group">
|
||||
<label for="field-ssn-number">Social Security Number</label>
|
||||
<div class="input-with-toggle">
|
||||
<input 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">
|
||||
<input
|
||||
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>
|
||||
</div>
|
||||
@@ -432,8 +629,12 @@
|
||||
</div>
|
||||
<div class="type-fields" data-for-types="password card bank address ssn">
|
||||
<div class="form-group">
|
||||
<label for="field-notes">Notes</label><textarea id="field-notes" rows="3"
|
||||
placeholder="Optional notes…"></textarea>
|
||||
<label for="field-notes">Notes</label
|
||||
><textarea
|
||||
id="field-notes"
|
||||
rows="3"
|
||||
placeholder="Optional notes…"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@@ -451,7 +652,11 @@
|
||||
<div class="modal modal-settings">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
</div>
|
||||
@@ -472,7 +677,13 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<button class="btn-primary" id="btn-mfa-confirm">Enable MFA</button>
|
||||
<button class="btn-secondary" id="btn-mfa-cancel-setup">
|
||||
@@ -481,8 +692,16 @@
|
||||
</div>
|
||||
<div id="mfa-disable-area" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="mfa-disable-code">Enter your current code to disable MFA</label>
|
||||
<input type="text" id="mfa-disable-code" inputmode="numeric" maxlength="6" placeholder="000000" />
|
||||
<label for="mfa-disable-code"
|
||||
>Enter your current code to disable MFA</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="mfa-disable-code"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
placeholder="000000"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn-primary" id="btn-mfa-disable-confirm">
|
||||
Disable MFA
|
||||
@@ -513,15 +732,30 @@
|
||||
<div id="change-password-area">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<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 class="form-group">
|
||||
<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>
|
||||
<p id="cp-error" class="form-error hidden"></p>
|
||||
<button class="btn-primary" id="btn-change-password">
|
||||
@@ -542,7 +776,11 @@
|
||||
<div id="recovery-code-display" class="hidden">
|
||||
<div class="recovery-code-box">
|
||||
<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>
|
||||
</div>
|
||||
@@ -566,8 +804,15 @@
|
||||
<div id="delete-account-area">
|
||||
<div id="delete-confirm-area" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="delete-password">Enter your master password to confirm</label>
|
||||
<input type="password" id="delete-password" autocomplete="current-password" placeholder="Master password" />
|
||||
<label for="delete-password"
|
||||
>Enter your master password to confirm</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="delete-password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Master password"
|
||||
/>
|
||||
</div>
|
||||
<p id="delete-error" class="form-error hidden"></p>
|
||||
<div class="modal-actions">
|
||||
@@ -590,7 +835,11 @@
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
</div>
|
||||
@@ -602,7 +851,11 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 class="modal-actions">
|
||||
<button type="button" class="btn-secondary" id="btn-cancel-share">
|
||||
@@ -619,7 +872,11 @@
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
</div>
|
||||
@@ -631,7 +888,11 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label for="emergency-wait-days">Wait period (days)</label>
|
||||
@@ -654,11 +915,21 @@
|
||||
</div>
|
||||
|
||||
<!-- ── 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-header">
|
||||
<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>
|
||||
</div>
|
||||
@@ -676,4 +947,4 @@
|
||||
<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/vault.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
+88
-51
@@ -12,7 +12,7 @@
|
||||
|
||||
// Default: 10 minutes. User can change via the Account view in the popup.
|
||||
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. */
|
||||
async function applyIdleInterval() {
|
||||
@@ -24,24 +24,27 @@ async function applyIdleInterval() {
|
||||
} else {
|
||||
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).
|
||||
applyIdleInterval();
|
||||
|
||||
chrome.idle.onStateChanged.addListener(async (newState) => {
|
||||
if (newState === 'idle' || newState === 'locked') {
|
||||
if (newState === "idle" || newState === "locked") {
|
||||
// Check if the user set "Never" before locking.
|
||||
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
|
||||
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.local.remove('vault_items_cs');
|
||||
await chrome.storage.local.remove("vault_items_cs");
|
||||
const tabs = await chrome.tabs.query({});
|
||||
tabs.forEach(tab => {
|
||||
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id });
|
||||
tabs.forEach((tab) => {
|
||||
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) {
|
||||
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) {
|
||||
chrome.action.setBadgeText({ text: '', tabId });
|
||||
chrome.action.setBadgeText({ text: "", tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
let hostname;
|
||||
try { hostname = new URL(url).hostname.replace(/^www\./, ''); }
|
||||
catch { chrome.action.setBadgeText({ text: '', tabId }); return; }
|
||||
try {
|
||||
hostname = new URL(url).hostname.replace(/^www\./, "");
|
||||
} catch {
|
||||
chrome.action.setBadgeText({ text: "", tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = vault_items.filter(item => {
|
||||
if (item.item_type !== 'password' || !item.plain?.url) return false;
|
||||
const matches = vault_items.filter((item) => {
|
||||
if (item.item_type !== "password" || !item.plain?.url) return false;
|
||||
try {
|
||||
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
|
||||
return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`);
|
||||
} catch { return false; }
|
||||
const h = new URL(item.plain.url).hostname.replace(/^www\./, "");
|
||||
return (
|
||||
h === hostname ||
|
||||
h.endsWith(`.${hostname}`) ||
|
||||
hostname.endsWith(`.${h}`)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (matches.length > 0) {
|
||||
chrome.action.setBadgeText({ text: String(matches.length), tabId });
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
|
||||
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
|
||||
} else {
|
||||
chrome.action.setBadgeText({ text: '', tabId });
|
||||
chrome.action.setBadgeText({ text: "", tabId });
|
||||
}
|
||||
} catch { /* tab may have closed */ }
|
||||
} catch {
|
||||
/* tab may have closed */
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllBadges() {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
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 }) => {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (tab.url?.startsWith('http')) updateBadgeForTab(tabId, tab.url);
|
||||
} catch { }
|
||||
if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -102,24 +117,30 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
// ── Message handler ──────────────────────────────────────────────────────────
|
||||
|
||||
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.
|
||||
if (msg.type === 'SET_IDLE_TIMEOUT') {
|
||||
if (msg.type === "SET_IDLE_TIMEOUT") {
|
||||
applyIdleInterval();
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Content script requests opening the vault tab (from suggestion dropdown).
|
||||
if (msg.type === 'OPEN_VAULT') {
|
||||
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
|
||||
if (msg.type === "OPEN_VAULT") {
|
||||
chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" });
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Content script requests navigating the popup to the generator view.
|
||||
if (msg.type === 'OPEN_GENERATOR') {
|
||||
chrome.storage.session.set({ popup_nav: 'generator' });
|
||||
if (msg.type === "OPEN_GENERATOR") {
|
||||
chrome.storage.session.set({ popup_nav: "generator" });
|
||||
chrome.action.openPopup().catch(() => {
|
||||
// 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
|
||||
// is open so chrome.storage.session is not wiped between popup openings.
|
||||
if (msg.type === 'KEEPALIVE') {
|
||||
if (msg.type === "KEEPALIVE") {
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Popup signals vault cache was refreshed → re-check badges AND forward
|
||||
// the decrypted items directly to all content scripts (avoids storage read).
|
||||
if (msg.type === 'VAULT_UPDATED') {
|
||||
if (msg.type === "VAULT_UPDATED") {
|
||||
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) {
|
||||
tabs.forEach(function (tab) {
|
||||
if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) {
|
||||
chrome.tabs.sendMessage(tab.id, payload).catch(function () { });
|
||||
if (
|
||||
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.
|
||||
if (msg.type === 'SAVE_CREDENTIALS') {
|
||||
if (msg.type === "SAVE_CREDENTIALS") {
|
||||
// Store in local storage so the prompt survives service worker restarts
|
||||
// and is guaranteed to be present when the user next opens the popup.
|
||||
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.
|
||||
if (msg.type === 'WEB_SESSION_SYNC') {
|
||||
if (msg.type === "WEB_SESSION_SYNC") {
|
||||
chrome.storage.session.set({
|
||||
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
|
||||
chrome.storage.local.set({
|
||||
@@ -175,28 +202,38 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
}
|
||||
|
||||
// Bridge: web app logged out → clear extension session.
|
||||
if (msg.type === 'WEB_SESSION_CLEAR') {
|
||||
chrome.storage.session.remove(['access_token', 'vault_key_jwk', 'vault_items', 'enc_key_salt']);
|
||||
chrome.storage.local.remove('refresh_token');
|
||||
if (msg.type === "WEB_SESSION_CLEAR") {
|
||||
chrome.storage.session.remove([
|
||||
"access_token",
|
||||
"vault_key_jwk",
|
||||
"vault_items",
|
||||
"enc_key_salt",
|
||||
]);
|
||||
chrome.storage.local.remove("refresh_token");
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Popup logged in via extension → inject session into open vault tabs.
|
||||
if (msg.type === 'EXT_SESSION_SYNC') {
|
||||
chrome.tabs.query({ url: 'https://pwkeeper.ngodanguyen.tech/*' }, tabs => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'INJECT_SESSION',
|
||||
access_token: msg.access_token,
|
||||
refresh_token: msg.refresh_token,
|
||||
enc_key_salt: msg.enc_key_salt,
|
||||
}).catch(() => { });
|
||||
});
|
||||
});
|
||||
if (msg.type === "EXT_SESSION_SYNC") {
|
||||
chrome.tabs.query(
|
||||
{ url: "https://pwkeeper.ngodanguyen.tech/*" },
|
||||
(tabs) => {
|
||||
tabs.forEach((tab) => {
|
||||
chrome.tabs
|
||||
.sendMessage(tab.id, {
|
||||
type: "INJECT_SESSION",
|
||||
access_token: msg.access_token,
|
||||
refresh_token: msg.refresh_token,
|
||||
enc_key_salt: msg.enc_key_salt,
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
},
|
||||
);
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user