mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-18 23:19:31 -05:00
Add TOTP autofill popup and field scaffolding (#1634)
This commit is contained in:
committed by
Leendert de Borst
parent
0cdfe5f4d9
commit
c858bc4e8a
@@ -6,11 +6,12 @@ import '@/entrypoints/contentScript/style.css';
|
||||
import { onMessage, sendMessage } from "webext-bridge/content-script";
|
||||
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { showSavePrompt, isSavePromptVisible, updateSavePromptLogin, getPersistedSavePromptState, restoreSavePromptFromState } from '@/entrypoints/contentScript/SavePrompt';
|
||||
import { initializeWebAuthnInterceptor } from '@/entrypoints/contentScript/WebAuthnInterceptor';
|
||||
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { DetectedFieldType } from '@/utils/formDetector/types/FormFields';
|
||||
import { LoginDetector } from '@/utils/loginDetector';
|
||||
import type { CapturedLogin } from '@/utils/loginDetector';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
@@ -407,7 +408,7 @@ export default defineContentScript({
|
||||
|
||||
// Only show popup if debounce time has passed
|
||||
if (popupDebounceTimeHasPassed()) {
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container, detectedFieldType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,6 +469,8 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedFieldType = formDetector.getDetectedFieldType();
|
||||
|
||||
/**
|
||||
* By default we check if the popup is not disabled (for current site) and if the field is autofill-triggerable
|
||||
* but if forceShow is true, we show the popup regardless.
|
||||
@@ -476,14 +479,17 @@ export default defineContentScript({
|
||||
|
||||
if (canShowPopup) {
|
||||
injectIcon(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container, detectedFieldType ?? undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup with auth check.
|
||||
* @param inputElement - The input element to show the popup for.
|
||||
* @param container - The container element.
|
||||
* @param fieldType - The detected field type (optional, defaults to regular autofill).
|
||||
*/
|
||||
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement) : Promise<void> {
|
||||
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement, fieldType?: DetectedFieldType) : Promise<void> {
|
||||
try {
|
||||
// Check auth status and pending migrations in a single call
|
||||
const { sendMessage } = await import('webext-bridge/content-script');
|
||||
@@ -513,8 +519,12 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
// No upgrade required, show normal autofill popup
|
||||
openAutofillPopup(inputElement, container);
|
||||
// Show appropriate popup based on field type
|
||||
if (fieldType === DetectedFieldType.Totp) {
|
||||
openTotpPopup(inputElement, container);
|
||||
} else {
|
||||
openAutofillPopup(inputElement, container);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AliasVault] Error checking vault status:', error);
|
||||
// Fall back to normal autofill popup if check fails
|
||||
|
||||
@@ -272,6 +272,41 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
actualInput.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill TOTP code into the input field.
|
||||
* Generates the code via background script and fills it.
|
||||
*
|
||||
* @param itemId - The item ID to generate TOTP code for.
|
||||
* @param input - The input element to fill the TOTP code into.
|
||||
*/
|
||||
export async function fillTotpCode(itemId: string, input: HTMLInputElement): Promise<void> {
|
||||
// Set debounce time to 300ms to prevent the popup from being shown again within 300ms because of autofill events.
|
||||
hidePopupFor(300);
|
||||
|
||||
// Reset auto-lock timer when autofilling
|
||||
sendMessage('RESET_AUTO_LOCK_TIMER', {}, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
|
||||
// Generate TOTP code via background
|
||||
const response = await sendMessage('GENERATE_TOTP_CODE', { itemId }, 'background') as {
|
||||
success: boolean;
|
||||
code?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (!response.success || !response.code) {
|
||||
console.error('Failed to generate TOTP code:', response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill the TOTP field
|
||||
input.value = response.code;
|
||||
|
||||
// Trigger input events for form validation
|
||||
triggerInputEvents(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger input events for an element to trigger form validation
|
||||
* which some websites require before the "continue" button is enabled.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CombinedFieldPatterns, CombinedGenderOptionPatterns, CombinedStopWords } from "./FieldPatterns";
|
||||
import { FormFields } from "./types/FormFields";
|
||||
import { DetectedFieldType, FormFields } from "./types/FormFields";
|
||||
|
||||
/**
|
||||
* Form detector.
|
||||
@@ -971,19 +971,19 @@ export class FormDetector {
|
||||
|
||||
/**
|
||||
* Get the detected field type for the clicked element.
|
||||
* Returns 'username', 'password', or 'email' if detected, null otherwise.
|
||||
* Returns a DetectedFieldType enum value if detected, null otherwise.
|
||||
* First checks for our custom data-av-field-type attribute (set on previous interactions),
|
||||
* then falls back to full field detection.
|
||||
*/
|
||||
public getDetectedFieldType(): string | null {
|
||||
public getDetectedFieldType(): DetectedFieldType | null {
|
||||
if (!this.clickedElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First check if we already detected and stored the field type
|
||||
const storedFieldType = this.clickedElement.getAttribute('data-av-field-type');
|
||||
if (storedFieldType) {
|
||||
return storedFieldType;
|
||||
if (storedFieldType && Object.values(DetectedFieldType).includes(storedFieldType as DetectedFieldType)) {
|
||||
return storedFieldType as DetectedFieldType;
|
||||
}
|
||||
|
||||
// Get the actual input element (handles shadow DOM)
|
||||
@@ -992,8 +992,8 @@ export class FormDetector {
|
||||
// Also check the actual element for stored field type
|
||||
if (actualElement !== this.clickedElement) {
|
||||
const actualStoredFieldType = actualElement.getAttribute('data-av-field-type');
|
||||
if (actualStoredFieldType) {
|
||||
return actualStoredFieldType;
|
||||
if (actualStoredFieldType && Object.values(DetectedFieldType).includes(actualStoredFieldType as DetectedFieldType)) {
|
||||
return actualStoredFieldType as DetectedFieldType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,26 +1008,26 @@ export class FormDetector {
|
||||
// Check if any of the elements is a username field
|
||||
const usernameFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text']);
|
||||
if (usernameFields.some(input => elementsToCheck.includes(input))) {
|
||||
return 'username';
|
||||
return DetectedFieldType.Username;
|
||||
}
|
||||
|
||||
// Check if any of the elements is a password field
|
||||
const passwordField = this.findPasswordField(formWrapper as HTMLFormElement | null);
|
||||
if ((passwordField.primary && elementsToCheck.includes(passwordField.primary)) ||
|
||||
(passwordField.confirm && elementsToCheck.includes(passwordField.confirm))) {
|
||||
return 'password';
|
||||
return DetectedFieldType.Password;
|
||||
}
|
||||
|
||||
// Check if any of the elements is an email field
|
||||
const emailFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.email, ['text', 'email']);
|
||||
if (emailFields.some(input => elementsToCheck.includes(input))) {
|
||||
return 'email';
|
||||
return DetectedFieldType.Email;
|
||||
}
|
||||
|
||||
// Check if any of the elements is a TOTP field
|
||||
const totpField = this.findTotpField(formWrapper as HTMLFormElement | null);
|
||||
if (totpField && elementsToCheck.includes(totpField)) {
|
||||
return 'totp';
|
||||
return DetectedFieldType.Totp;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* Detected field type for autofill trigger fields.
|
||||
* Used to determine which popup to show and how to handle the field.
|
||||
*/
|
||||
export enum DetectedFieldType {
|
||||
Username = 'username',
|
||||
Password = 'password',
|
||||
Email = 'email',
|
||||
Totp = 'totp'
|
||||
}
|
||||
|
||||
export type FormFields = {
|
||||
form: HTMLFormElement | null;
|
||||
emailField: HTMLInputElement | null;
|
||||
|
||||
Reference in New Issue
Block a user