feat: masked passwords UI with vault-gated reveal
Passwords imported from password managers are now: - Displayed as dots (••••••••) by default in both options and popup - Separated into their own "Passwords" section in options page - Only revealable by entering the vault encryption password - Always masked in the popup mappings list (no reveal there) Options page: - New Passwords section with locked/unlocked states - Reveal button validates against vault encryption password - Hide button re-masks all password values - Password mappings excluded from the general Mappings table - Delete and enable/disable controls work while masked Popup: - Password-category mappings show dots for real value - Password category added to mapping add form dropdown Storage: - Added 'password' to default categories list https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
autoAddDetected: true,
|
autoAddDetected: true,
|
||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
||||||
browserSync: false,
|
browserSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -463,6 +463,7 @@
|
|||||||
<option value="ssn">SSN</option>
|
<option value="ssn">SSN</option>
|
||||||
<option value="dob">DOB</option>
|
<option value="dob">DOB</option>
|
||||||
<option value="domain">Domain</option>
|
<option value="domain">Domain</option>
|
||||||
|
<option value="password">Password</option>
|
||||||
<option value="general">General</option>
|
<option value="general">General</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
@@ -473,6 +474,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Passwords</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Imported passwords are automatically redacted when sent to AI services.
|
||||||
|
Password values are hidden by default — enter your vault password to reveal them.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="passwordsLocked" style="margin-bottom:12px">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<span style="font-size:12px;color:#6b7280">🔒 Password values are hidden</span>
|
||||||
|
<input type="password" id="passwordRevealKey" placeholder="Vault password to reveal" autocomplete="current-password"
|
||||||
|
style="font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px;width:180px">
|
||||||
|
<button class="btn btn-sm" id="btnRevealPasswords">Reveal</button>
|
||||||
|
</div>
|
||||||
|
<div id="passwordRevealStatus" style="font-size:11px;margin-top:4px;min-height:14px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="passwordsUnlocked" style="display:none;margin-bottom:12px">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<span style="font-size:12px;color:#b45309">🔓 Passwords visible</span>
|
||||||
|
<button class="btn btn-sm" id="btnHidePasswords">Hide</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="mapping-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Password</th>
|
||||||
|
<th>Redacted As</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="passwordTableBody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="noPasswordsMsg" style="text-align:center;color:#9ca3af;padding:16px;font-size:13px;display:none">
|
||||||
|
No passwords imported. Use Transfer Data → Import to add passwords from your password manager.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Custom Domains</h2>
|
<h2>Custom Domains</h2>
|
||||||
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
|
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
|
||||||
|
|||||||
+99
-2
@@ -10,6 +10,7 @@ import api from '../lib/browser-polyfill.js';
|
|||||||
|
|
||||||
let mappings = [];
|
let mappings = [];
|
||||||
let settings = {};
|
let settings = {};
|
||||||
|
let passwordsRevealed = false;
|
||||||
|
|
||||||
const $ = (sel) => document.querySelector(sel);
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#browserSync').checked = settings.browserSync === true;
|
$('#browserSync').checked = settings.browserSync === true;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
|
renderPasswords();
|
||||||
renderDomains();
|
renderDomains();
|
||||||
renderLog();
|
renderLog();
|
||||||
|
|
||||||
@@ -326,6 +328,36 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
renderMappings();
|
renderMappings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Password reveal/hide
|
||||||
|
$('#btnRevealPasswords').addEventListener('click', async () => {
|
||||||
|
const pw = $('#passwordRevealKey').value;
|
||||||
|
if (!pw) {
|
||||||
|
setPasswordRevealStatus('Enter your vault password.', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await SilentSendSync.authenticate(pw);
|
||||||
|
if (result.success) {
|
||||||
|
passwordsRevealed = true;
|
||||||
|
$('#passwordsLocked').style.display = 'none';
|
||||||
|
$('#passwordsUnlocked').style.display = 'block';
|
||||||
|
$('#passwordRevealKey').value = '';
|
||||||
|
renderPasswords();
|
||||||
|
} else {
|
||||||
|
setPasswordRevealStatus('Wrong password.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnHidePasswords').addEventListener('click', () => {
|
||||||
|
passwordsRevealed = false;
|
||||||
|
$('#passwordsLocked').style.display = 'block';
|
||||||
|
$('#passwordsUnlocked').style.display = 'none';
|
||||||
|
renderPasswords();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#passwordRevealKey').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') $('#btnRevealPasswords').click();
|
||||||
|
});
|
||||||
|
|
||||||
// Clear log
|
// Clear log
|
||||||
$('#btnClearLog').addEventListener('click', async () => {
|
$('#btnClearLog').addEventListener('click', async () => {
|
||||||
await Storage.clearLog();
|
await Storage.clearLog();
|
||||||
@@ -369,13 +401,15 @@ async function addMapping() {
|
|||||||
|
|
||||||
function renderMappings() {
|
function renderMappings() {
|
||||||
const tbody = $('#mappingTableBody');
|
const tbody = $('#mappingTableBody');
|
||||||
|
// Exclude password-category mappings — they have their own section
|
||||||
|
const nonPasswordMappings = mappings.filter(m => m.category !== 'password');
|
||||||
|
|
||||||
if (mappings.length === 0) {
|
if (nonPasswordMappings.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#9ca3af;padding:24px">No mappings configured</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#9ca3af;padding:24px">No mappings configured</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = mappings
|
tbody.innerHTML = nonPasswordMappings
|
||||||
.map(
|
.map(
|
||||||
(m) => `
|
(m) => `
|
||||||
<tr data-id="${m.id}">
|
<tr data-id="${m.id}">
|
||||||
@@ -415,6 +449,69 @@ function renderMappings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Passwords Section ---
|
||||||
|
|
||||||
|
function renderPasswords() {
|
||||||
|
const passwordMappings = mappings.filter(m => m.category === 'password');
|
||||||
|
const tbody = $('#passwordTableBody');
|
||||||
|
const noMsg = $('#noPasswordsMsg');
|
||||||
|
|
||||||
|
if (passwordMappings.length === 0) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
noMsg.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
noMsg.style.display = 'none';
|
||||||
|
|
||||||
|
tbody.innerHTML = passwordMappings.map(m => {
|
||||||
|
const displayReal = passwordsRevealed
|
||||||
|
? escapeHtml(m.real)
|
||||||
|
: '••••••••';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr data-id="${m.id}">
|
||||||
|
<td class="real" style="font-family:monospace;font-size:12px">${displayReal}</td>
|
||||||
|
<td class="sub" style="font-size:12px">${escapeHtml(m.substitute)}</td>
|
||||||
|
<td>
|
||||||
|
<label class="toggle" style="width:32px;height:18px">
|
||||||
|
<input type="checkbox" class="toggle-pw-enabled" ${m.enabled ? 'checked' : ''}>
|
||||||
|
<span class="toggle-slider" style="border-radius:18px"></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td><button class="btn btn-sm btn-danger btn-delete-pw">×</button></td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Bind delete
|
||||||
|
tbody.querySelectorAll('.btn-delete-pw').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const id = btn.closest('tr').dataset.id;
|
||||||
|
await Storage.deleteMapping(id);
|
||||||
|
mappings = mappings.filter(m => m.id !== id);
|
||||||
|
renderPasswords();
|
||||||
|
renderMappings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind toggle
|
||||||
|
tbody.querySelectorAll('.toggle-pw-enabled').forEach(cb => {
|
||||||
|
cb.addEventListener('change', async () => {
|
||||||
|
const id = cb.closest('tr').dataset.id;
|
||||||
|
await Storage.updateMapping(id, { enabled: cb.checked });
|
||||||
|
const m = mappings.find(m => m.id === id);
|
||||||
|
if (m) m.enabled = cb.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPasswordRevealStatus(msg, type) {
|
||||||
|
const el = $('#passwordRevealStatus');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
|
||||||
|
}
|
||||||
|
|
||||||
async function renderLog() {
|
async function renderLog() {
|
||||||
const log = await Storage.getLog();
|
const log = await Storage.getLog();
|
||||||
$('#logCount').textContent = `${log.length} entries`;
|
$('#logCount').textContent = `${log.length} entries`;
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<option value="ssn">SSN</option>
|
<option value="ssn">SSN</option>
|
||||||
<option value="dob">DOB</option>
|
<option value="dob">DOB</option>
|
||||||
<option value="domain">Domain</option>
|
<option value="domain">Domain</option>
|
||||||
|
<option value="password">Password</option>
|
||||||
<option value="general">General</option>
|
<option value="general">General</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
|
|||||||
+1
-1
@@ -585,7 +585,7 @@ function renderMappings() {
|
|||||||
(m) => `
|
(m) => `
|
||||||
<div class="mapping-item" data-id="${m.id}">
|
<div class="mapping-item" data-id="${m.id}">
|
||||||
<div class="mapping-values">
|
<div class="mapping-values">
|
||||||
<span class="mapping-real">${escapeHtml(m.real)}</span>
|
<span class="mapping-real">${m.category === 'password' ? '••••••••' : escapeHtml(m.real)}</span>
|
||||||
→
|
→
|
||||||
<span class="mapping-sub">${escapeHtml(m.substitute)}</span>
|
<span class="mapping-sub">${escapeHtml(m.substitute)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user