mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 15:56:11 -04:00
Auto copy TOTP to clipboard, update settings (#1891)
This commit is contained in:
committed by
Leendert de Borst
parent
49cab65631
commit
6e33694b2c
@@ -6,12 +6,13 @@ import '@/entrypoints/contentScript/style.css';
|
||||
import { onMessage, sendMessage } from "webext-bridge/content-script";
|
||||
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { showSavePrompt, showAddUrlPrompt, isSavePromptVisible, updateSavePromptLogin, getPersistedSavePromptState, restoreSavePromptFromState, restoreAddUrlPromptFromState } from '@/entrypoints/contentScript/SavePrompt';
|
||||
import { initializeWebAuthnInterceptor } from '@/entrypoints/contentScript/WebAuthnInterceptor';
|
||||
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { DetectedFieldType } from '@/utils/formDetector/types/FormFields';
|
||||
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
|
||||
import { LoginDetector } from '@/utils/loginDetector';
|
||||
import type { CapturedLogin, LastAutofilledCredential } from '@/utils/loginDetector';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
@@ -503,17 +504,24 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
// Only inject icon and show popup if autofill popup is enabled
|
||||
if (await isAutoShowPopupEnabled()) {
|
||||
// Store our detected field type for subsequent clicks
|
||||
inputElement.setAttribute('data-av-field-type', detectedFieldType);
|
||||
// Check if site allows autofill (site-specific disabled sites)
|
||||
if (!await isSiteAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(inputElement, container);
|
||||
// Check if we should show autofill UI for this field type
|
||||
if (!await shouldShowAutofillUi(detectedFieldType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show popup if debounce time has passed
|
||||
if (popupDebounceTimeHasPassed()) {
|
||||
await showPopupWithAuthCheck(inputElement, container, detectedFieldType);
|
||||
}
|
||||
// Store our detected field type for subsequent clicks
|
||||
inputElement.setAttribute('data-av-field-type', detectedFieldType);
|
||||
|
||||
injectIcon(inputElement, container, detectedFieldType);
|
||||
|
||||
// Only show popup if debounce time has passed
|
||||
if (popupDebounceTimeHasPassed()) {
|
||||
await showPopupWithAuthCheck(inputElement, container, detectedFieldType);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -558,6 +566,44 @@ export default defineContentScript({
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if autofill is disabled for the current site (site-specific settings only).
|
||||
* @returns True if site allows autofill, false if site has disabled it
|
||||
*/
|
||||
async function isSiteAllowed(): Promise<boolean> {
|
||||
const disabledSites = await LocalPreferencesService.getDisabledSites();
|
||||
const temporaryDisabledSites = await LocalPreferencesService.getTemporaryDisabledSites();
|
||||
const currentHostname = window.location.hostname;
|
||||
|
||||
if (disabledSites.includes(currentHostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const temporaryDisabledUntil = temporaryDisabledSites[currentHostname];
|
||||
if (temporaryDisabledUntil && Date.now() < temporaryDisabledUntil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should show autofill UI (icon/popup) for a given field type.
|
||||
* Checks the appropriate toggle setting based on field type.
|
||||
*
|
||||
* @param fieldType - The detected field type
|
||||
* @returns True if we should show UI for this field type, false otherwise
|
||||
*/
|
||||
async function shouldShowAutofillUi(fieldType: DetectedFieldType | null): Promise<boolean> {
|
||||
// For TOTP fields, check TOTP autofill toggle
|
||||
if (fieldType === DetectedFieldType.Totp) {
|
||||
return await LocalPreferencesService.getTotpAutofillEnabled();
|
||||
}
|
||||
|
||||
// For credential fields, check credential autofill toggle
|
||||
return await LocalPreferencesService.getGlobalAutofillPopupEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup for element.
|
||||
*/
|
||||
@@ -576,13 +622,18 @@ export default defineContentScript({
|
||||
const detectedFieldType = formDetector.getDetectedFieldType();
|
||||
|
||||
/**
|
||||
* By default we check if the popup is not disabled (for current site) and if the field is autofill-triggerable
|
||||
* By default we check if the site allows autofill and if the field is autofill-triggerable
|
||||
* but if forceShow is true, we show the popup regardless.
|
||||
*/
|
||||
const canShowPopup = forceShow || (await isAutoShowPopupEnabled() && formDetector.isAutofillTriggerableField());
|
||||
const canShowPopup = forceShow || (await isSiteAllowed() && formDetector.isAutofillTriggerableField());
|
||||
|
||||
if (canShowPopup) {
|
||||
injectIcon(inputElement, container);
|
||||
// Check field-type-specific settings (credential vs TOTP toggles)
|
||||
if (!await shouldShowAutofillUi(detectedFieldType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(inputElement, container, detectedFieldType ?? undefined);
|
||||
await showPopupWithAuthCheck(inputElement, container, detectedFieldType ?? undefined);
|
||||
}
|
||||
}
|
||||
@@ -625,7 +676,12 @@ export default defineContentScript({
|
||||
|
||||
// Show appropriate popup based on field type
|
||||
if (fieldType === DetectedFieldType.Totp) {
|
||||
openTotpPopup(inputElement, container);
|
||||
// Check if TOTP autofill is enabled
|
||||
const totpAutofillEnabled = await LocalPreferencesService.getTotpAutofillEnabled();
|
||||
if (totpAutofillEnabled) {
|
||||
openTotpPopup(inputElement, container);
|
||||
}
|
||||
// If disabled, don't show any popup (user can rely on clipboard auto-copy)
|
||||
} else {
|
||||
openAutofillPopup(inputElement, container);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { sendMessage } from 'webext-bridge/content-script';
|
||||
|
||||
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { openAutofillPopup, openTotpPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
import { LOGO_MARK_SVG } from '@/utils/constants/logo';
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { itemToCredential, FieldKey } from '@/utils/dist/core/models/vault';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { FormFiller } from '@/utils/formDetector/FormFiller';
|
||||
import { DetectedFieldType } from '@/utils/formDetector/types/FormFields';
|
||||
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
|
||||
import type { LastAutofilledCredential } from '@/utils/loginDetector';
|
||||
import { ClickValidator } from '@/utils/security/ClickValidator';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
@@ -143,6 +145,31 @@ export async function fillItem(item: Item, input: HTMLInputElement): Promise<voi
|
||||
sendMessage('SET_RECENTLY_SELECTED', { itemId: item.Id, domain: window.location.hostname }, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
|
||||
// Auto-copy TOTP to clipboard if enabled and item has TOTP after autofill.
|
||||
const autoCopyTotpEnabled = await LocalPreferencesService.getAutoCopyTotpOnAutofill();
|
||||
if (autoCopyTotpEnabled) {
|
||||
try {
|
||||
// Generate TOTP code via background
|
||||
const response = await sendMessage('GENERATE_TOTP_CODE', { itemId: item.Id }, 'background') as {
|
||||
success: boolean;
|
||||
code?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (response.success && response.code) {
|
||||
// Copy TOTP code to clipboard
|
||||
await navigator.clipboard.writeText(response.code);
|
||||
|
||||
// Notify background script that clipboard was copied to start countdown
|
||||
sendMessage('CLIPBOARD_COPIED', { value: response.code }, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Silently fail in case TOTP code is not available which is possible.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,8 +216,11 @@ function findActualInput(element: HTMLElement): HTMLInputElement {
|
||||
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
* @param input - The input element to inject icon for
|
||||
* @param container - The container element
|
||||
* @param fieldType - The detected field type (optional, defaults to regular autofill)
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement, fieldType?: DetectedFieldType): void {
|
||||
// Find the actual input element to use for positioning
|
||||
const actualInput = findActualInput(input);
|
||||
|
||||
@@ -272,7 +302,13 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
}
|
||||
|
||||
setTimeout(() => actualInput.focus(), 0);
|
||||
openAutofillPopup(actualInput, container);
|
||||
|
||||
// Open the appropriate popup based on field type
|
||||
if (fieldType === DetectedFieldType.Totp) {
|
||||
openTotpPopup(actualInput, container);
|
||||
} else {
|
||||
openAutofillPopup(actualInput, container);
|
||||
}
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
|
||||
@@ -1277,40 +1277,6 @@ function createItemList(items: Item[], input: HTMLInputElement, rootContainer: H
|
||||
/**
|
||||
* Check if auto-popup is disabled for current site
|
||||
*/
|
||||
export async function isAutoShowPopupEnabled(): Promise<boolean> {
|
||||
const disabledSites = await LocalPreferencesService.getDisabledSites();
|
||||
const temporaryDisabledSites = await LocalPreferencesService.getTemporaryDisabledSites();
|
||||
const globalPopupEnabled = await LocalPreferencesService.getGlobalAutofillPopupEnabled();
|
||||
|
||||
const currentHostname = window.location.hostname;
|
||||
|
||||
if (!globalPopupEnabled) {
|
||||
// Popup is disabled for all sites.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disabledSites.includes(currentHostname)) {
|
||||
// Popup is permanently disabled for current site.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check temporary disable
|
||||
const temporaryDisabledUntil = temporaryDisabledSites[currentHostname];
|
||||
if (temporaryDisabledUntil && Date.now() < temporaryDisabledUntil) {
|
||||
// Popup is temporarily disabled for current site.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check time-based dismissal
|
||||
const dismissUntil = await LocalPreferencesService.getVaultLockedDismissUntil();
|
||||
if (dismissUntil && Date.now() < dismissUntil) {
|
||||
// Popup is dismissed for a certain amount of time.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-popup for current site
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ const KEYS = {
|
||||
PASSKEY_DISABLED_SITES: 'local:aliasvault_passkey_disabled_sites',
|
||||
|
||||
// Global toggles
|
||||
GLOBAL_AUTOFILL_POPUP_ENABLED: 'local:aliasvault_global_autofill_popup_enabled',
|
||||
CREDENTIAL_AUTOFILL_POPUP_ENABLED: 'local:aliasvault_global_autofill_popup_enabled',
|
||||
GLOBAL_CONTEXT_MENU_ENABLED: 'local:aliasvault_global_context_menu_enabled',
|
||||
PASSKEY_PROVIDER_ENABLED: 'local:aliasvault_passkey_provider_enabled',
|
||||
|
||||
@@ -94,22 +94,6 @@ export const LocalPreferencesService = {
|
||||
await storage.setItem(KEYS.AUTO_CLOSE_UNLOCK_POPUP, enabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether the global autofill popup is enabled.
|
||||
* @returns Whether autofill popup is globally enabled. Defaults to true.
|
||||
*/
|
||||
async getGlobalAutofillPopupEnabled(): Promise<boolean> {
|
||||
const value = await storage.getItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED) as boolean | null;
|
||||
return value !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set whether the global autofill popup is enabled.
|
||||
*/
|
||||
async setGlobalAutofillPopupEnabled(enabled: boolean): Promise<void> {
|
||||
await storage.setItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED, enabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the autofill matching mode.
|
||||
* @returns The matching mode. Defaults to DEFAULT.
|
||||
@@ -456,6 +440,22 @@ export const LocalPreferencesService = {
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get whether the global autofill popup is enabled.
|
||||
* @returns Whether autofill popup is globally enabled. Defaults to true.
|
||||
*/
|
||||
async getGlobalAutofillPopupEnabled(): Promise<boolean> {
|
||||
const value = await storage.getItem(KEYS.CREDENTIAL_AUTOFILL_POPUP_ENABLED) as boolean | null;
|
||||
return value !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set whether the global autofill popup is enabled.
|
||||
*/
|
||||
async setGlobalAutofillPopupEnabled(enabled: boolean): Promise<void> {
|
||||
await storage.setItem(KEYS.CREDENTIAL_AUTOFILL_POPUP_ENABLED, enabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether TOTP autofill is enabled.
|
||||
* @returns Whether TOTP autofill is enabled. Defaults to true (enabled by default).
|
||||
|
||||
Reference in New Issue
Block a user