mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 15:18:02 -04:00
Show autofill popup dismiss button when vault is locked (#682)
This commit is contained in:
committed by
Leendert de Borst
parent
852d9b5e98
commit
4fdf7ce92c
@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
|
||||
|
||||
export default defineBackground({
|
||||
@@ -17,6 +17,7 @@ export default defineBackground({
|
||||
);
|
||||
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
|
||||
@@ -11,6 +11,23 @@ import { VaultResponse as messageVaultResponse } from '../../utils/types/messagi
|
||||
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
|
||||
import { DefaultEmailDomainResponse as messageDefaultEmailDomainResponse } from '../../utils/types/messaging/DefaultEmailDomainResponse';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData !== null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the vault in browser storage.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupDisabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { canShowPopup, injectIcon } from './contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
@@ -42,22 +42,16 @@ export default defineContentScript({
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
|
||||
if (target.tagName === 'INPUT' &&
|
||||
textInputTypes.includes(target.type) &&
|
||||
!target.dataset.aliasvaultIgnore) {
|
||||
if (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
|
||||
const isDisabled = await isAutoShowPopupDisabled();
|
||||
const canShow = canShowPopup();
|
||||
|
||||
// Only show popup if it's not disabled and the popup can be shown
|
||||
if (!isDisabled && canShow) {
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(target, container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ let popupDebounceTime = 0;
|
||||
/**
|
||||
* Check if popup can be shown based on debounce time.
|
||||
*/
|
||||
export function canShowPopup() : boolean {
|
||||
export function popupDebounceTimeHasPassed() : boolean {
|
||||
if (Date.now() < popupDebounceTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -443,19 +443,25 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
* Create vault locked popup.
|
||||
*/
|
||||
export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: HTMLElement): void {
|
||||
const popup = createBasePopup(input, rootContainer);
|
||||
popup.classList.add('av-vault-locked');
|
||||
|
||||
// Make the whole popup clickable to open the main extension login popup.
|
||||
popup.addEventListener('click', () => {
|
||||
/**
|
||||
* Handle unlock click.
|
||||
*/
|
||||
const handleUnlockClick = () : void => {
|
||||
sendMessage('OPEN_POPUP', {}, 'background');
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
}
|
||||
|
||||
const popup = createBasePopup(input, rootContainer);
|
||||
popup.classList.add('av-vault-locked');
|
||||
|
||||
// Create container for message and button
|
||||
const container = document.createElement('div');
|
||||
container.className = 'av-vault-locked-container';
|
||||
|
||||
// Make the entire container clickable
|
||||
container.addEventListener('click', handleUnlockClick);
|
||||
container.style.cursor = 'pointer';
|
||||
|
||||
// Add message
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'av-vault-locked-message';
|
||||
@@ -472,10 +478,37 @@ export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: H
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
container.appendChild(button);
|
||||
|
||||
// Add the container to the popup
|
||||
popup.appendChild(container);
|
||||
|
||||
// Add close button as a separate element positioned to the right
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.className = 'av-button av-button-close av-vault-locked-close';
|
||||
closeButton.title = 'Dismiss popup';
|
||||
closeButton.innerHTML = `
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Position the close button to the right of the container
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.right = '8px';
|
||||
closeButton.style.top = '50%';
|
||||
closeButton.style.transform = 'translateY(-50%)';
|
||||
|
||||
// Handle close button click
|
||||
closeButton.addEventListener('click', async (e) => {
|
||||
e.stopPropagation(); // Prevent opening the unlock popup
|
||||
await dismissVaultLockedPopup();
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
|
||||
popup.appendChild(closeButton);
|
||||
|
||||
/**
|
||||
* Add event listener to document to close popup when clicking outside.
|
||||
*/
|
||||
@@ -600,17 +633,35 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
|
||||
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
|
||||
export const GLOBAL_POPUP_ENABLED_KEY = 'local:aliasvault_global_popup_enabled';
|
||||
export const VAULT_LOCKED_DISMISS_UNTIL_KEY = 'local:aliasvault_vault_locked_dismiss_until';
|
||||
|
||||
/**
|
||||
* Check if auto-popup is disabled for current site
|
||||
*/
|
||||
export async function isAutoShowPopupDisabled(): Promise<boolean> {
|
||||
export async function isAutoShowPopupEnabled(): Promise<boolean> {
|
||||
const disabledSites = await storage.getItem(DISABLED_SITES_KEY) as string[] ?? [];
|
||||
const globalPopupEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) ?? true;
|
||||
|
||||
const currentHostname = window.location.hostname;
|
||||
|
||||
return !globalPopupEnabled || disabledSites.includes(currentHostname);
|
||||
if (!globalPopupEnabled) {
|
||||
// Popup is disabled for all sites.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disabledSites.includes(currentHostname)) {
|
||||
// Popup is disabled for current site.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check time-based dismissal
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
if (dismissUntil && Date.now() < dismissUntil) {
|
||||
// Popup is dismissed for a certain amount of time.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -926,3 +977,22 @@ function detectMimeType(bytes: Uint8Array): string {
|
||||
|
||||
return 'image/x-icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss vault locked popup for 4 hours if user is logged in but vault is locked,
|
||||
* or for 3 days if user is not logged in.
|
||||
*/
|
||||
export async function dismissVaultLockedPopup(): Promise<void> {
|
||||
// First check if user is logged in but vault is locked, or not logged in at all
|
||||
const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as { isLoggedIn: boolean, isVaultLocked: boolean };
|
||||
|
||||
if (authStatus.isLoggedIn && authStatus.isVaultLocked) {
|
||||
// User is logged in but vault is locked - dismiss for 4 hours
|
||||
const fourHoursFromNow = Date.now() + (4 * 60 * 60 * 1000);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, fourHoursFromNow);
|
||||
} else if (!authStatus.isLoggedIn) {
|
||||
// User is not logged in - dismiss for 3 days
|
||||
const threeDaysFromNow = Date.now() + (3 * 24 * 60 * 60 * 1000);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, threeDaysFromNow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ body {
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 0; /* Enable text truncation */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.av-credential-logo {
|
||||
@@ -91,8 +91,8 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Enable text truncation */
|
||||
margin-right: 8px; /* Add space between text and popout icon */
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.av-service-name {
|
||||
@@ -121,7 +121,7 @@ body {
|
||||
margin-right: 16px;
|
||||
opacity: 0.6;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0; /* Prevent icon from shrinking */
|
||||
flex-shrink: 0;
|
||||
color: #ffffff;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
@@ -209,7 +209,6 @@ body {
|
||||
outline: none;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.av-search-input::placeholder {
|
||||
@@ -224,7 +223,7 @@ body {
|
||||
/* Vault Locked Popup */
|
||||
.av-vault-locked {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-vault-locked:hover {
|
||||
@@ -234,28 +233,46 @@ body {
|
||||
.av-vault-locked-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-vault-locked-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
padding-right: 32px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-vault-locked-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #d68338;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-vault-locked-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
@@ -404,4 +421,4 @@ body {
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
|
||||
import { useDb } from './DbContext';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
@@ -66,6 +67,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
*/
|
||||
const login = useCallback(async () : Promise<void> => {
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
@@ -19,6 +20,7 @@ const AuthSettings: React.FC = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -27,6 +29,15 @@ const AuthSettings: React.FC = () => {
|
||||
const loadStoredSettings = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
const clientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
|
||||
if (dismissUntil) {
|
||||
setIsGloballyEnabled(false);
|
||||
} else {
|
||||
setIsGloballyEnabled(globallyEnabled);
|
||||
}
|
||||
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
|
||||
|
||||
if (matchingOption) {
|
||||
@@ -74,6 +85,23 @@ const AuthSettings: React.FC = () => {
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
if (newGloballyEnabled) {
|
||||
// Reset all disabled sites when enabling globally
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}
|
||||
|
||||
setIsGloballyEnabled(newGloballyEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-6">
|
||||
@@ -124,6 +152,23 @@ const AuthSettings: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isGloballyEnabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
|
||||
@@ -144,11 +144,11 @@ const Settings: React.FC = () => {
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,18 +164,18 @@ const Settings: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Open popup on: {settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? 'Popup is active' : 'Popup is disabled'}
|
||||
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import SrpUtility from '../utils/SrpUtility';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
@@ -75,6 +77,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
|
||||
Reference in New Issue
Block a user