mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-28 16:02:22 -05:00
1283 lines
44 KiB
TypeScript
1283 lines
44 KiB
TypeScript
import { FormDetector } from './src/utils/form-detector/FormDetector';
|
|
import { Credential } from './src/types/Credential';
|
|
import { IdentityGeneratorEn } from './src/generators/Identity/implementations/IdentityGeneratorEn';
|
|
import { PasswordGenerator } from './src/generators/Password/PasswordGenerator';
|
|
|
|
type CredentialResponse = {
|
|
status: 'OK' | 'LOCKED';
|
|
credentials?: Credential[];
|
|
}
|
|
|
|
const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==';
|
|
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
|
<path d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z" fill="#EEC170"/>
|
|
<path d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z" fill="#EEC170"/>
|
|
<path d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z" fill="#EEC170"/>
|
|
<path d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z" fill="#EEC170"/>
|
|
</svg>`;
|
|
|
|
const DISABLED_SITES_KEY = 'aliasvault_disabled_sites';
|
|
const ICON_HTML = `
|
|
<div class="aliasvault-input-icon" style="
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 8px;
|
|
margin: auto;
|
|
cursor: pointer;
|
|
width: 20px;
|
|
height: 20px;
|
|
z-index: 999999;
|
|
">
|
|
<img src="data:image/svg+xml;base64,${btoa(aliasvaultIconSvg)}" style="width: 100%; height: 100%;" />
|
|
</div>
|
|
`;
|
|
|
|
const getLoadingHtml = (message: string): string => `
|
|
<div style="
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
gap: 8px;
|
|
">
|
|
<svg class="animate-spin" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10" stroke-opacity="0.25"/>
|
|
<path d="M12 2C6.47715 2 2 6.47715 2 12" stroke-opacity="1"/>
|
|
</svg>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Declare handleClickOutside at module scope
|
|
let handleClickOutside: (event: MouseEvent) => void;
|
|
|
|
/**
|
|
* Check if the current theme is dark.
|
|
*/
|
|
function isDarkMode(): boolean {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
|
|
/**
|
|
* Listen for input field focus
|
|
*/
|
|
document.addEventListener('focusin', async (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
if (target.tagName === 'INPUT' && !target.dataset.aliasvaultIgnore) {
|
|
const isDisabled = await isAutoPopupDisabled();
|
|
if (!isDisabled) {
|
|
showCredentialPopup(target);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Show credential popup
|
|
*/
|
|
function showCredentialPopup(input: HTMLInputElement) : void {
|
|
const formDetector = new FormDetector(document);
|
|
const forms = formDetector.detectForms();
|
|
|
|
if (!forms.length) return;
|
|
|
|
// Add keydown event listener for Enter key
|
|
const handleEnterKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
removeExistingPopup();
|
|
// Remove the event listener to clean up
|
|
document.removeEventListener('keydown', handleEnterKey);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEnterKey);
|
|
|
|
// Request credentials from background script
|
|
chrome.runtime.sendMessage({ type: 'GET_CREDENTIALS_FOR_URL', url: window.location.href }, (response: CredentialResponse) => {
|
|
switch (response.status) {
|
|
case 'OK':
|
|
if (response.credentials?.length) {
|
|
createPopup(input, response.credentials);
|
|
}
|
|
break;
|
|
|
|
case 'LOCKED':
|
|
createStatusPopup(input, 'AliasVault is locked.');
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Filter credentials based on current URL and page context
|
|
*/
|
|
function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
|
|
const urlObject = new URL(currentUrl);
|
|
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
|
|
|
|
// 1. Exact URL match
|
|
let filtered = credentials.filter(cred =>
|
|
cred.ServiceUrl?.toLowerCase() === currentUrl.toLowerCase()
|
|
);
|
|
|
|
// 2. Base URL match with fuzzy domain comparison if no exact matches
|
|
if (filtered.length === 0) {
|
|
filtered = credentials.filter(cred => {
|
|
if (!cred.ServiceUrl) return false;
|
|
try {
|
|
const credUrlObject = new URL(cred.ServiceUrl);
|
|
const currentUrlObject = new URL(baseUrl);
|
|
|
|
// Extract root domains by splitting on dots and taking last two parts
|
|
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
|
|
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
|
|
|
|
// Get root domain (last two parts, e.g., 'dumpert.nl')
|
|
const credRootDomain = credDomainParts.slice(-2).join('.');
|
|
const currentRootDomain = currentDomainParts.slice(-2).join('.');
|
|
|
|
// Compare protocols and root domains
|
|
return credUrlObject.protocol === currentUrlObject.protocol &&
|
|
credRootDomain === currentRootDomain;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 3. Page title word match if still no matches
|
|
if (filtered.length === 0 && pageTitle.length > 0) {
|
|
const titleWords = pageTitle.toLowerCase().split(/\s+/);
|
|
filtered = credentials.filter(cred =>
|
|
titleWords.some(word =>
|
|
cred.ServiceName.toLowerCase().includes(word)
|
|
)
|
|
);
|
|
}
|
|
|
|
// Show max 3 results
|
|
return filtered.slice(0, 3);
|
|
}
|
|
|
|
/**
|
|
* Create auto-fill popup
|
|
*/
|
|
function createPopup(input: HTMLInputElement, credentials: Credential[]) : void {
|
|
// Remove existing popup and its event listeners
|
|
removeExistingPopup();
|
|
|
|
const popup = document.createElement('div');
|
|
popup.id = 'aliasvault-credential-popup';
|
|
|
|
// Get input width
|
|
const inputWidth = input.offsetWidth;
|
|
|
|
// Set popup width to match input width, with min/max constraints
|
|
const popupWidth = Math.max(360, Math.min(640, inputWidth));
|
|
|
|
popup.style.cssText = `
|
|
position: absolute;
|
|
z-index: 999999;
|
|
background: ${isDarkMode() ? '#1f2937' : 'white'};
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 4px ${isDarkMode() ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.2)'};
|
|
padding: 8px 0;
|
|
width: ${popupWidth}px;
|
|
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
|
|
// Filter credentials based on current page context
|
|
const filteredCredentials = filterCredentials(
|
|
credentials,
|
|
window.location.href,
|
|
document.title
|
|
);
|
|
|
|
// Add credentials to popup using the shared function
|
|
const credentialElements = createCredentialList(filteredCredentials, input);
|
|
credentialElements.forEach(element => popup.appendChild(element));
|
|
|
|
// Add divider
|
|
const divider = document.createElement('div');
|
|
divider.style.cssText = `
|
|
height: 1px;
|
|
background: ${isDarkMode() ? '#374151' : '#e5e7eb'};
|
|
margin: 8px 0;
|
|
`;
|
|
popup.appendChild(divider);
|
|
|
|
// Add action buttons container
|
|
const actionContainer = document.createElement('div');
|
|
actionContainer.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
`;
|
|
|
|
// Create New button
|
|
const createButton = document.createElement('button');
|
|
createButton.style.cssText = `
|
|
flex: 1;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
background: ${isDarkMode() ? '#374151' : '#f3f4f6'};
|
|
color: ${isDarkMode() ? '#e5e7eb' : '#374151'};
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
`;
|
|
createButton.innerHTML = `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
New
|
|
`;
|
|
createButton.addEventListener('click', async () => {
|
|
const serviceName = await createEditNamePopup(document.title);
|
|
if (!serviceName) return; // User cancelled
|
|
|
|
// Create a new popup for loading state
|
|
const loadingPopup = document.createElement('div');
|
|
loadingPopup.id = 'aliasvault-credential-popup';
|
|
|
|
// Get input width
|
|
const inputWidth = input.offsetWidth;
|
|
const popupWidth = Math.max(360, Math.min(640, inputWidth));
|
|
|
|
loadingPopup.style.cssText = `
|
|
position: absolute;
|
|
z-index: 999999;
|
|
background: ${isDarkMode() ? '#1f2937' : 'white'};
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 4px ${isDarkMode() ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.2)'};
|
|
width: ${popupWidth}px;
|
|
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
|
|
// Position popup below input
|
|
const rect = input.getBoundingClientRect();
|
|
loadingPopup.style.top = `${rect.bottom + window.scrollY + 2}px`;
|
|
loadingPopup.style.left = `${rect.left + window.scrollX}px`;
|
|
|
|
// Add loading content
|
|
loadingPopup.innerHTML = getLoadingHtml('Creating new identity...');
|
|
|
|
// Remove existing popup and show loading popup
|
|
removeExistingPopup();
|
|
document.body.appendChild(loadingPopup);
|
|
|
|
try {
|
|
// Retrieve default email domain from background
|
|
const response = await new Promise<{ domain: string }>((resolve) => {
|
|
chrome.runtime.sendMessage({ type: 'GET_DEFAULT_EMAIL_DOMAIN' }, resolve);
|
|
});
|
|
|
|
const domain = response.domain;
|
|
|
|
// Generate new identity locally
|
|
const identityGenerator = new IdentityGeneratorEn();
|
|
const identity = await identityGenerator.generateRandomIdentity();
|
|
|
|
const passwordGenerator = new PasswordGenerator();
|
|
const password = passwordGenerator.generateRandomPassword();
|
|
|
|
// Extract favicon from page and get the bytes
|
|
const favicon = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]') as HTMLLinkElement;
|
|
let faviconBytes: ArrayBuffer | null = null;
|
|
|
|
if (favicon?.href) {
|
|
try {
|
|
const response = await fetch(favicon.href);
|
|
faviconBytes = await response.arrayBuffer();
|
|
} catch (error) {
|
|
console.error('Error fetching favicon:', error);
|
|
}
|
|
}
|
|
|
|
// Submit new identity to backend to persist in db
|
|
const credential: Credential = {
|
|
Id: '',
|
|
ServiceName: serviceName,
|
|
ServiceUrl: window.location.href,
|
|
Email: `${identity.emailPrefix}@${domain}`,
|
|
Logo: faviconBytes ? new Uint8Array(faviconBytes) : undefined,
|
|
Username: identity.nickName,
|
|
Password: password,
|
|
Notes: '',
|
|
Alias: {
|
|
FirstName: identity.firstName,
|
|
LastName: identity.lastName,
|
|
NickName: identity.nickName,
|
|
BirthDate: identity.birthDate.toISOString(),
|
|
Gender: identity.gender,
|
|
Email: `${identity.emailPrefix}@${domain}`
|
|
}
|
|
};
|
|
|
|
chrome.runtime.sendMessage({ type: 'CREATE_IDENTITY', credential }, () => {
|
|
// Refresh the popup to show new identity
|
|
showCredentialPopup(input);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating identity:', error);
|
|
loadingPopup.innerHTML = `
|
|
<div style="padding: 16px; color: #ef4444;">
|
|
Failed to create identity. Please try again.
|
|
</div>
|
|
`;
|
|
setTimeout(() => {
|
|
removeExistingPopup();
|
|
}, 2000);
|
|
}
|
|
});
|
|
|
|
// Create search input instead of button
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = 'Search vault...';
|
|
searchInput.style.cssText = `
|
|
flex: 2;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
background: ${isDarkMode() ? '#374151' : '#f3f4f6'};
|
|
color: ${isDarkMode() ? '#e5e7eb' : '#374151'};
|
|
font-size: 14px;
|
|
border: 1px solid ${isDarkMode() ? '#4b5563' : '#e5e7eb'};
|
|
outline: none;
|
|
`;
|
|
|
|
// Add focus styles
|
|
searchInput.addEventListener('focus', () => {
|
|
searchInput.style.borderColor = '#2563eb';
|
|
searchInput.style.boxShadow = '0 0 0 2px rgba(37, 99, 235, 0.2)';
|
|
});
|
|
|
|
searchInput.addEventListener('blur', () => {
|
|
searchInput.style.borderColor = isDarkMode() ? '#4b5563' : '#e5e7eb';
|
|
searchInput.style.boxShadow = 'none';
|
|
});
|
|
|
|
// Handle search input
|
|
let searchTimeout: NodeJS.Timeout;
|
|
searchInput.addEventListener('input', () => {
|
|
clearTimeout(searchTimeout);
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
|
|
// Request credentials from background script
|
|
chrome.runtime.sendMessage({ type: 'GET_CREDENTIALS_FOR_URL', url: window.location.href }, (response: CredentialResponse) => {
|
|
if (response.status === 'OK' && response.credentials) {
|
|
let filteredCredentials;
|
|
|
|
if (searchTerm === '') {
|
|
// If search is empty, use original URL-based filtering
|
|
filteredCredentials = filterCredentials(
|
|
response.credentials,
|
|
window.location.href,
|
|
document.title
|
|
);
|
|
} else {
|
|
// Otherwise filter based on search term
|
|
filteredCredentials = response.credentials.filter(cred =>
|
|
cred.ServiceName.toLowerCase().includes(searchTerm) ||
|
|
cred.Username.toLowerCase().includes(searchTerm) ||
|
|
cred.Email.toLowerCase().includes(searchTerm) ||
|
|
cred.ServiceUrl?.toLowerCase().includes(searchTerm)
|
|
);
|
|
|
|
// Show max 3 results for search
|
|
if (filteredCredentials.length > 3) {
|
|
filteredCredentials = filteredCredentials.slice(0, 3);
|
|
}
|
|
}
|
|
|
|
// Update popup content with filtered results
|
|
updatePopupContent(popup, filteredCredentials, input);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close button
|
|
const closeButton = document.createElement('button');
|
|
closeButton.style.cssText = `
|
|
padding: 6px;
|
|
border-radius: 4px;
|
|
background: ${isDarkMode() ? '#374151' : '#f3f4f6'};
|
|
color: ${isDarkMode() ? '#e5e7eb' : '#374151'};
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
closeButton.innerHTML = `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
`;
|
|
closeButton.addEventListener('click', async () => {
|
|
await disableAutoPopup();
|
|
removeExistingPopup();
|
|
});
|
|
|
|
actionContainer.appendChild(searchInput);
|
|
actionContainer.appendChild(createButton);
|
|
actionContainer.appendChild(closeButton);
|
|
popup.appendChild(actionContainer);
|
|
|
|
// Define handleClickOutside
|
|
handleClickOutside = (event: MouseEvent) : void => {
|
|
const popup = document.getElementById('aliasvault-credential-popup');
|
|
const target = event.target as Node;
|
|
|
|
// If popup doesn't exist, remove the listener
|
|
if (!popup) {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
return;
|
|
}
|
|
|
|
// Ignore clicks on the popup and its children
|
|
if (popup.contains(target)) {
|
|
return;
|
|
}
|
|
|
|
// Check if click target is an input field
|
|
const inputFields = document.querySelectorAll('input');
|
|
for (const input of inputFields) {
|
|
if (input.contains(target)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
removeExistingPopup();
|
|
};
|
|
|
|
// Add the event listener for clicking outside
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
// Position popup below input
|
|
const rect = input.getBoundingClientRect();
|
|
popup.style.top = `${rect.bottom + window.scrollY + 2}px`;
|
|
popup.style.left = `${rect.left + window.scrollX}px`;
|
|
|
|
document.body.appendChild(popup);
|
|
}
|
|
|
|
/**
|
|
* Create status popup. TODO: refactor to use same popup basic structure for all popup types.
|
|
*/
|
|
function createStatusPopup(input: HTMLInputElement, message: string): void {
|
|
// Remove existing popup if any
|
|
removeExistingPopup();
|
|
|
|
const popup = document.createElement('div');
|
|
popup.id = 'aliasvault-credential-popup';
|
|
|
|
// Get input width
|
|
const inputWidth = input.offsetWidth;
|
|
|
|
// Set popup width to match input width, with min/max constraints
|
|
const popupWidth = Math.max(240, Math.min(640, inputWidth));
|
|
|
|
popup.style.cssText = `
|
|
position: absolute;
|
|
z-index: 999999;
|
|
background: ${isDarkMode() ? '#1f2937' : 'white'};
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 4px ${isDarkMode() ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.2)'};
|
|
padding: 12px 16px;
|
|
width: ${popupWidth}px;
|
|
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
|
|
// Add hover effect to the entire popup
|
|
popup.addEventListener('mouseenter', () => {
|
|
popup.style.backgroundColor = isDarkMode() ? '#374151' : '#f0f0f0';
|
|
});
|
|
|
|
popup.addEventListener('mouseleave', () => {
|
|
popup.style.backgroundColor = isDarkMode() ? '#1f2937' : 'white';
|
|
});
|
|
|
|
// Create container for message and button
|
|
const container = document.createElement('div');
|
|
container.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
`;
|
|
|
|
// Add message
|
|
const messageElement = document.createElement('div');
|
|
messageElement.style.cssText = `
|
|
color: ${isDarkMode() ? '#d1d5db' : '#666'};
|
|
font-size: 14px;
|
|
padding-right: 32px;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
messageElement.textContent = message;
|
|
container.appendChild(messageElement);
|
|
|
|
// Add unlock button with SVG icon
|
|
const button = document.createElement('button');
|
|
button.title = 'Unlock AliasVault';
|
|
button.style.cssText = `
|
|
position: absolute;
|
|
right: 0;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #0066cc;
|
|
border-radius: 4px;
|
|
`;
|
|
button.innerHTML = `
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
`;
|
|
|
|
// Make the whole container clickable to open the popup.
|
|
container.addEventListener('click', () => {
|
|
chrome.runtime.sendMessage({ type: 'OPEN_POPUP' });
|
|
removeExistingPopup();
|
|
});
|
|
|
|
container.appendChild(button);
|
|
popup.appendChild(container);
|
|
|
|
// Position popup below input
|
|
const rect = input.getBoundingClientRect();
|
|
popup.style.top = `${rect.bottom + window.scrollY + 2}px`;
|
|
popup.style.left = `${rect.left + window.scrollX}px`;
|
|
|
|
/**
|
|
* Add event listener to document to close popup when clicking outside.
|
|
*/
|
|
const handleClickOutside = (event: MouseEvent): void => {
|
|
if (!popup.contains(event.target as Node)) {
|
|
removeExistingPopup();
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
};
|
|
|
|
setTimeout(() => {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}, 100);
|
|
|
|
document.body.appendChild(popup);
|
|
}
|
|
|
|
/**
|
|
* Remove existing popup
|
|
*/
|
|
function removeExistingPopup() : void {
|
|
const existing = document.getElementById('aliasvault-credential-popup');
|
|
if (existing) {
|
|
// Remove the mousedown event listener before removing the popup
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
existing.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fill credential
|
|
*/
|
|
function fillCredential(credential: Credential) : void {
|
|
const formDetector = new FormDetector(document);
|
|
const forms = formDetector.detectForms();
|
|
|
|
if (!forms.length) return;
|
|
|
|
const form = forms[0];
|
|
if (form.usernameField) {
|
|
form.usernameField.value = credential.Username;
|
|
triggerInputEvents(form.usernameField);
|
|
}
|
|
if (form.passwordField) {
|
|
form.passwordField.value = credential.Password;
|
|
triggerInputEvents(form.passwordField);
|
|
}
|
|
if (form.passwordConfirmField) {
|
|
form.passwordConfirmField.value = credential.Password;
|
|
triggerInputEvents(form.passwordConfirmField);
|
|
}
|
|
if (form.emailField) {
|
|
form.emailField.value = credential.Email;
|
|
triggerInputEvents(form.emailField);
|
|
}
|
|
if (form.emailConfirmField) {
|
|
form.emailConfirmField.value = credential.Email;
|
|
triggerInputEvents(form.emailConfirmField);
|
|
}
|
|
if (form.firstNameField) {
|
|
form.firstNameField.value = credential.Alias.FirstName;
|
|
triggerInputEvents(form.firstNameField);
|
|
}
|
|
if (form.lastNameField) {
|
|
form.lastNameField.value = credential.Alias.LastName;
|
|
triggerInputEvents(form.lastNameField);
|
|
}
|
|
|
|
// Handle birthdate with input events
|
|
if (form.birthdateField.single) {
|
|
if (credential.Alias.BirthDate) {
|
|
const birthDate = new Date(credential.Alias.BirthDate);
|
|
const day = birthDate.getDate().toString().padStart(2, '0');
|
|
const month = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
|
const year = birthDate.getFullYear().toString();
|
|
|
|
let formattedDate = '';
|
|
switch (form.birthdateField.format) {
|
|
case 'dd-mm-yyyy':
|
|
formattedDate = `${day}-${month}-${year}`;
|
|
break;
|
|
case 'mm-dd-yyyy':
|
|
formattedDate = `${month}-${day}-${year}`;
|
|
break;
|
|
case 'yyyy-mm-dd':
|
|
default:
|
|
formattedDate = `${year}-${month}-${day}`;
|
|
break;
|
|
}
|
|
|
|
form.birthdateField.single.value = formattedDate;
|
|
triggerInputEvents(form.birthdateField.single);
|
|
}
|
|
} else {
|
|
if (credential.Alias.BirthDate) {
|
|
const birthDate = new Date(credential.Alias.BirthDate);
|
|
if (form.birthdateField.day) {
|
|
form.birthdateField.day.value = birthDate.getDate().toString().padStart(2, '0');
|
|
triggerInputEvents(form.birthdateField.day);
|
|
}
|
|
if (form.birthdateField.month) {
|
|
form.birthdateField.month.value = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
|
triggerInputEvents(form.birthdateField.month);
|
|
}
|
|
if (form.birthdateField.year) {
|
|
form.birthdateField.year.value = birthDate.getFullYear().toString();
|
|
triggerInputEvents(form.birthdateField.year);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle gender with input events
|
|
switch (form.genderField.type) {
|
|
case 'select':
|
|
if (form.genderField.field) {
|
|
switch (credential.Alias.Gender) {
|
|
case 'Male':
|
|
(form.genderField.field as HTMLSelectElement).value = 'M';
|
|
break;
|
|
case 'Female':
|
|
(form.genderField.field as HTMLSelectElement).value = 'F';
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'radio':
|
|
const radioButtons = form.genderField.radioButtons;
|
|
if (!radioButtons) break;
|
|
|
|
let selectedRadio: HTMLInputElement | null = null;
|
|
if (credential.Alias.Gender === 'Male' && radioButtons.male) {
|
|
radioButtons.male.checked = true;
|
|
selectedRadio = radioButtons.male;
|
|
} else if (credential.Alias.Gender === 'Female' && radioButtons.female) {
|
|
radioButtons.female.checked = true;
|
|
selectedRadio = radioButtons.female;
|
|
} else if (credential.Alias.Gender === 'Other' && radioButtons.other) {
|
|
radioButtons.other.checked = true;
|
|
selectedRadio = radioButtons.other;
|
|
}
|
|
|
|
if (selectedRadio) {
|
|
triggerInputEvents(selectedRadio);
|
|
}
|
|
break;
|
|
case 'text':
|
|
if (form.genderField.field && credential.Alias.Gender) {
|
|
(form.genderField.field as HTMLInputElement).value = credential.Alias.Gender;
|
|
triggerInputEvents(form.genderField.field as HTMLInputElement);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger input events for an element to trigger form validation
|
|
* which some websites require before the "continue" button is enabled.
|
|
*/
|
|
function triggerInputEvents(element: HTMLInputElement) : void {
|
|
// Basic events
|
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
// For radio buttons, we need additional events in order for form validation
|
|
// to be triggered correctly.
|
|
if (element.type === 'radio') {
|
|
// Click events
|
|
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base64 encode binary data.
|
|
*/
|
|
function base64Encode(buffer: Uint8Array): string | null {
|
|
if (!buffer || typeof buffer !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Convert object to array of numbers
|
|
const byteArray = Object.values(buffer);
|
|
|
|
// Convert to binary string
|
|
const binary = String.fromCharCode.apply(null, byteArray as number[]);
|
|
|
|
// Use btoa to encode binary string to base64
|
|
return btoa(binary);
|
|
} catch (error) {
|
|
console.error('Error encoding to base64:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if auto-popup is disabled for current site
|
|
*/
|
|
async function isAutoPopupDisabled(): Promise<boolean> {
|
|
const result = await chrome.storage.local.get(DISABLED_SITES_KEY);
|
|
const disabledSites = result[DISABLED_SITES_KEY] || [];
|
|
return disabledSites.includes(window.location.hostname);
|
|
}
|
|
|
|
/**
|
|
* Disable auto-popup for current site
|
|
*/
|
|
async function disableAutoPopup(): Promise<void> {
|
|
const result = await chrome.storage.local.get(DISABLED_SITES_KEY);
|
|
const disabledSites = result[DISABLED_SITES_KEY] || [];
|
|
if (!disabledSites.includes(window.location.hostname)) {
|
|
disabledSites.push(window.location.hostname);
|
|
await chrome.storage.local.set({ [DISABLED_SITES_KEY]: disabledSites });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inject icon for a focused input element
|
|
*/
|
|
function injectIcon(input: HTMLInputElement): void {
|
|
// Don't inject if already exists
|
|
if (document.querySelector(`[data-icon-for="${input.id}"]`)) {
|
|
return;
|
|
}
|
|
|
|
const iconDiv = document.createElement('div');
|
|
iconDiv.innerHTML = ICON_HTML;
|
|
const icon = iconDiv.firstElementChild as HTMLElement;
|
|
|
|
// Get input's position and dimensions
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
// Position icon absolutely relative to viewport
|
|
icon.style.cssText = `
|
|
position: fixed;
|
|
z-index: 9999;
|
|
cursor: pointer;
|
|
top: ${inputRect.top + window.scrollY + (inputRect.height - 24) / 2}px;
|
|
right: ${window.innerWidth - (inputRect.right + window.scrollX) + 8}px;
|
|
width: 24px;
|
|
height: 24px;
|
|
pointer-events: auto;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease-in-out;
|
|
`;
|
|
|
|
icon.setAttribute('data-icon-for', input.id);
|
|
|
|
icon.addEventListener('click', () => {
|
|
showCredentialPopup(input);
|
|
});
|
|
|
|
// Add to body
|
|
document.body.appendChild(icon);
|
|
|
|
// Fade in the icon
|
|
requestAnimationFrame(() => {
|
|
icon.style.opacity = '1';
|
|
});
|
|
|
|
// Remove icon when input loses focus, except when clicking the icon
|
|
const handleBlur = (e: FocusEvent) => {
|
|
// Don't remove if clicking the icon itself
|
|
if (e.relatedTarget === icon) {
|
|
return;
|
|
}
|
|
|
|
// Fade out and remove icon
|
|
icon.style.opacity = '0';
|
|
setTimeout(() => {
|
|
icon.remove();
|
|
input.removeEventListener('blur', handleBlur);
|
|
}, 200); // Match transition duration
|
|
};
|
|
|
|
input.addEventListener('blur', handleBlur);
|
|
}
|
|
|
|
/**
|
|
* Listen for input field focus
|
|
*/
|
|
document.addEventListener('focusin', async (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
if (target.tagName === 'INPUT' && !target.dataset.aliasvaultIgnore) {
|
|
const formDetector = new FormDetector(document);
|
|
const forms = formDetector.detectForms();
|
|
|
|
if (!forms.length) return;
|
|
|
|
injectIcon(target);
|
|
|
|
const isDisabled = await isAutoPopupDisabled();
|
|
if (!isDisabled) {
|
|
showCredentialPopup(target);
|
|
}
|
|
}
|
|
});
|
|
|
|
const createEditNamePopup = (defaultName: string): Promise<string | null> => {
|
|
// Close existing popup
|
|
removeExistingPopup();
|
|
|
|
return new Promise((resolve) => {
|
|
// Create modal overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 999999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
|
|
const popup = document.createElement('div');
|
|
popup.style.cssText = `
|
|
position: relative;
|
|
z-index: 1000000;
|
|
background: ${isDarkMode() ? '#1f2937' : 'white'};
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
|
0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
padding: 24px;
|
|
width: 400px;
|
|
max-width: 90vw;
|
|
transform: scale(0.95);
|
|
opacity: 0;
|
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
|
|
popup.innerHTML = `
|
|
<h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: ${isDarkMode() ? '#f8f9fa' : '#000000'}">
|
|
New alias name
|
|
</h3>
|
|
<input
|
|
type="text"
|
|
id="service-name-input"
|
|
data-aliasvault-ignore="true"
|
|
value="${defaultName}"
|
|
style="
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
margin-bottom: 24px;
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
|
border-radius: 6px;
|
|
background: ${isDarkMode() ? '#374151' : 'white'};
|
|
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
|
|
font-size: 14px;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
"
|
|
>
|
|
<div style="display: flex; justify-content: flex-end; gap: 12px;">
|
|
<button id="cancel-btn" style="
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid ${isDarkMode() ? '#374151' : '#e5e7eb'};
|
|
background: transparent;
|
|
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: all 0.2s ease;
|
|
">Cancel</button>
|
|
<button id="save-btn" style="
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: #2563eb;
|
|
color: white;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: all 0.2s ease;
|
|
">Save</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(popup);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Add hover and focus styles
|
|
const input = popup.querySelector('#service-name-input') as HTMLInputElement;
|
|
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
|
|
const cancelBtn = popup.querySelector('#cancel-btn') as HTMLButtonElement;
|
|
|
|
input.addEventListener('focus', () => {
|
|
input.style.borderColor = '#2563eb';
|
|
input.style.boxShadow = '0 0 0 3px rgba(37, 99, 235, 0.1)';
|
|
});
|
|
|
|
input.addEventListener('blur', () => {
|
|
input.style.borderColor = isDarkMode() ? '#374151' : '#ccc';
|
|
input.style.boxShadow = 'none';
|
|
});
|
|
|
|
saveBtn.addEventListener('mouseenter', () => {
|
|
saveBtn.style.background = '#1d4ed8';
|
|
saveBtn.style.transform = 'translateY(-1px)';
|
|
});
|
|
|
|
saveBtn.addEventListener('mouseleave', () => {
|
|
saveBtn.style.background = '#2563eb';
|
|
saveBtn.style.transform = 'translateY(0)';
|
|
});
|
|
|
|
cancelBtn.addEventListener('mouseenter', () => {
|
|
cancelBtn.style.background = isDarkMode() ? '#374151' : '#f3f4f6';
|
|
});
|
|
|
|
cancelBtn.addEventListener('mouseleave', () => {
|
|
cancelBtn.style.background = 'transparent';
|
|
});
|
|
|
|
// Animate in
|
|
requestAnimationFrame(() => {
|
|
popup.style.transform = 'scale(1)';
|
|
popup.style.opacity = '1';
|
|
});
|
|
|
|
// Select input text
|
|
input.select();
|
|
|
|
// Add variable to track if text is being selected
|
|
let isSelecting = false;
|
|
|
|
// Add mousedown handler to input
|
|
input.addEventListener('mousedown', () => {
|
|
isSelecting = true;
|
|
});
|
|
|
|
// Add mouseup handler to document
|
|
document.addEventListener('mouseup', () => {
|
|
// Use setTimeout to ensure click handler runs after mouseup
|
|
setTimeout(() => {
|
|
isSelecting = false;
|
|
}, 0);
|
|
});
|
|
|
|
const closePopup = (value: string | null) => {
|
|
popup.style.transform = 'scale(0.95)';
|
|
popup.style.opacity = '0';
|
|
setTimeout(() => {
|
|
overlay.remove();
|
|
resolve(value);
|
|
}, 200);
|
|
};
|
|
|
|
// Handle save
|
|
saveBtn.addEventListener('click', () => {
|
|
const value = input.value.trim();
|
|
if (value) {
|
|
closePopup(value);
|
|
}
|
|
});
|
|
|
|
// Handle cancel
|
|
cancelBtn.addEventListener('click', () => {
|
|
closePopup(null);
|
|
});
|
|
|
|
// Handle Enter key
|
|
input.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Enter') {
|
|
const value = input.value.trim();
|
|
if (value) {
|
|
closePopup(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle click outside
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) {
|
|
// Check if there's any text selected in the input
|
|
const selectedText = input.value.substring(input.selectionStart || 0, input.selectionEnd || 0);
|
|
|
|
// Only close if no text is selected
|
|
if (!selectedText) {
|
|
closePopup(null);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
// Add URL change detection using the History API
|
|
let lastUrl = window.location.href;
|
|
|
|
// Create observer to watch for URL changes
|
|
const urlObserver = new MutationObserver(() => {
|
|
if (window.location.href !== lastUrl) {
|
|
lastUrl = window.location.href;
|
|
removeExistingPopup();
|
|
}
|
|
});
|
|
|
|
// Start observing
|
|
urlObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
|
|
// Also listen for popstate events (back/forward navigation)
|
|
window.addEventListener('popstate', () => {
|
|
removeExistingPopup();
|
|
});
|
|
/**
|
|
* Create credential list content for popup
|
|
*/
|
|
function createCredentialList(credentials: Credential[], input: HTMLInputElement): HTMLElement[] {
|
|
const elements: HTMLElement[] = [];
|
|
|
|
if (credentials.length > 0) {
|
|
credentials.forEach(cred => {
|
|
const item = document.createElement('div');
|
|
item.style.cssText = `
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
transition: background-color 0.2s ease;
|
|
border-radius: 4px;
|
|
margin: 0 4px;
|
|
`;
|
|
|
|
// Create container for credential info (logo + username)
|
|
const credentialInfo = document.createElement('div');
|
|
credentialInfo.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
flex-grow: 1;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s ease;
|
|
`;
|
|
|
|
const imgElement = document.createElement('img');
|
|
imgElement.style.width = '20px';
|
|
imgElement.style.height = '20px';
|
|
|
|
// Handle base64 image data
|
|
if (cred.Logo) {
|
|
try {
|
|
const base64Logo = base64Encode(cred.Logo);
|
|
imgElement.src = `data:image/x-icon;base64,${base64Logo}`;
|
|
} catch (error) {
|
|
console.error('Error setting logo:', error);
|
|
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
|
|
}
|
|
} else {
|
|
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
|
|
}
|
|
|
|
credentialInfo.appendChild(imgElement);
|
|
const credTextContainer = document.createElement('div');
|
|
credTextContainer.style.cssText = `
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
min-width: 0; /* Enable text truncation in flex container */
|
|
`;
|
|
|
|
// Service name (primary text)
|
|
const serviceName = document.createElement('div');
|
|
serviceName.style.cssText = `
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
font-size: 14px;
|
|
text-overflow: ellipsis;
|
|
color: ${isDarkMode() ? '#f3f4f6' : '#111827'};
|
|
`;
|
|
serviceName.textContent = cred.ServiceName;
|
|
|
|
// Details container (secondary text)
|
|
const detailsContainer = document.createElement('div');
|
|
detailsContainer.style.cssText = `
|
|
font-size: 0.85em;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
font-size: 12px;
|
|
text-overflow: ellipsis;
|
|
color: ${isDarkMode() ? '#9ca3af' : '#6b7280'};
|
|
`;
|
|
|
|
// Combine full name (if available) and username
|
|
const details = [];
|
|
if (cred.Alias?.FirstName && cred.Alias?.LastName) {
|
|
details.push(`${cred.Alias.FirstName} ${cred.Alias.LastName}`);
|
|
}
|
|
details.push(cred.Username);
|
|
detailsContainer.textContent = details.join(' · ');
|
|
|
|
credTextContainer.appendChild(serviceName);
|
|
credTextContainer.appendChild(detailsContainer);
|
|
credentialInfo.appendChild(credTextContainer);
|
|
|
|
// Add popout icon
|
|
const popoutIcon = document.createElement('div');
|
|
popoutIcon.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px;
|
|
opacity: 0.6;
|
|
border-radius: 4px;
|
|
`;
|
|
popoutIcon.innerHTML = `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
</svg>
|
|
`;
|
|
|
|
// Add hover effects
|
|
popoutIcon.addEventListener('mouseenter', () => {
|
|
popoutIcon.style.opacity = '1';
|
|
popoutIcon.style.backgroundColor = isDarkMode() ? '#ffffff' : '#000000';
|
|
popoutIcon.style.color = isDarkMode() ? '#000000' : '#ffffff';
|
|
});
|
|
|
|
popoutIcon.addEventListener('mouseleave', () => {
|
|
popoutIcon.style.opacity = '0.6';
|
|
popoutIcon.style.backgroundColor = 'transparent';
|
|
popoutIcon.style.color = isDarkMode() ? '#ffffff' : '#000000';
|
|
});
|
|
|
|
// Handle popout click
|
|
popoutIcon.addEventListener('click', (e) => {
|
|
e.stopPropagation(); // Prevent credential fill
|
|
chrome.runtime.sendMessage({
|
|
type: 'OPEN_POPUP_WITH_CREDENTIAL',
|
|
credentialId: cred.Id
|
|
});
|
|
removeExistingPopup();
|
|
});
|
|
|
|
item.appendChild(credentialInfo);
|
|
item.appendChild(popoutIcon);
|
|
|
|
// Update hover effect for the entire item
|
|
item.addEventListener('mouseenter', () => {
|
|
item.style.backgroundColor = isDarkMode() ? '#2d3748' : '#f3f4f6';
|
|
popoutIcon.style.opacity = '1';
|
|
});
|
|
|
|
item.addEventListener('mouseleave', () => {
|
|
item.style.backgroundColor = 'transparent';
|
|
popoutIcon.style.opacity = '0.6';
|
|
});
|
|
|
|
// Update click handler to only trigger on credentialInfo
|
|
credentialInfo.addEventListener('click', () => {
|
|
fillCredential(cred);
|
|
removeExistingPopup();
|
|
});
|
|
|
|
elements.push(item);
|
|
});
|
|
} else {
|
|
const noMatches = document.createElement('div');
|
|
noMatches.style.cssText = `
|
|
padding: 8px 16px;
|
|
color: ${isDarkMode() ? '#9ca3af' : '#6b7280'};
|
|
font-style: italic;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
`;
|
|
noMatches.textContent = 'No matches found';
|
|
elements.push(noMatches);
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
// Update updatePopupContent to use the new function
|
|
function updatePopupContent(popup: HTMLElement, credentials: Credential[], input: HTMLInputElement) {
|
|
// Store the action container
|
|
const actionContainer = popup.lastElementChild;
|
|
|
|
// Clear all content except the action container
|
|
while (popup.firstChild && popup.firstChild !== actionContainer) {
|
|
popup.removeChild(popup.firstChild);
|
|
}
|
|
|
|
// Add credentials using the shared function
|
|
const credentialElements = createCredentialList(credentials, input);
|
|
credentialElements.forEach(element => {
|
|
popup.insertBefore(element, actionContainer);
|
|
});
|
|
|
|
// Add divider before action container
|
|
const divider = document.createElement('div');
|
|
divider.style.cssText = `
|
|
height: 1px;
|
|
background: ${isDarkMode() ? '#374151' : '#e5e7eb'};
|
|
margin: 8px 0;
|
|
`;
|
|
popup.insertBefore(divider, actionContainer);
|
|
}
|