Add storage utility for fallback purposes, refactor EmailDomainField public email domain usage (#1404)

This commit is contained in:
Leendert de Borst
2025-12-15 12:16:12 +01:00
parent 93c8ca3ff2
commit 08912772ee
6 changed files with 89 additions and 54 deletions

View File

@@ -5,6 +5,7 @@ import type { EncryptionKeyDerivationParams } from '@/utils/dist/core/models/met
import type { Vault, VaultResponse, VaultPostResponse } from '@/utils/dist/core/models/webapi';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import { SqliteClient } from '@/utils/SqliteClient';
import { getItemWithFallback } from '@/utils/StorageUtility';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
import { CredentialsResponse as messageCredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
@@ -201,9 +202,10 @@ export async function handleGetVault(
// Read from local: storage for persistent vault access
const encryptedVault = await storage.getItem('local:encryptedVault') as string;
const publicEmailDomains = await storage.getItem('local:publicEmailDomains') as string[];
const privateEmailDomains = await storage.getItem('local:privateEmailDomains') as string[];
const hiddenPrivateEmailDomains = await storage.getItem('local:hiddenPrivateEmailDomains') as string[] ?? [];
// Use fallback for keys migrated from session: to local: in v0.26.0
const publicEmailDomains = await getItemWithFallback<string[]>('local:publicEmailDomains');
const privateEmailDomains = await getItemWithFallback<string[]>('local:privateEmailDomains');
const hiddenPrivateEmailDomains = await getItemWithFallback<string[]>('local:hiddenPrivateEmailDomains') ?? [];
const serverRevision = await storage.getItem('local:serverRevision') as number | null;
if (!encryptedVault) {
@@ -433,7 +435,7 @@ export async function getEmailAddressesForVault(
const credentials = sqliteClient.getAllCredentials();
// Get metadata from local: storage
const privateEmailDomains = await storage.getItem('local:privateEmailDomains') as string[];
const privateEmailDomains = await getItemWithFallback<string[]>('local:privateEmailDomains') ?? [];
const emailAddresses = credentials
.filter(cred => cred.Alias?.Email != null)
@@ -526,15 +528,8 @@ export async function handleGetEncryptionKey(
*/
export async function handleGetEncryptionKeyDerivationParams(
) : Promise<EncryptionKeyDerivationParams | null> {
// Try local: storage first (current location since offline support)
let params = await storage.getItem('local:encryptionKeyDerivationParams') as EncryptionKeyDerivationParams | null;
// Fall back to session: storage for backwards compatibility
if (!params) {
params = await storage.getItem('session:encryptionKeyDerivationParams') as EncryptionKeyDerivationParams | null;
}
return params;
// Get metadata from storage
return await getItemWithFallback<EncryptionKeyDerivationParams>('local:encryptionKeyDerivationParams');
}
/**

View File

@@ -8,8 +8,7 @@ import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { AppInfo } from '@/utils/AppInfo';
import type { ApiErrorResponse, MailboxEmail } from '@/utils/dist/core/models/webapi';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import { storage } from '#imports';
import { getItemWithFallback } from '@/utils/StorageUtility';
type EmailPreviewProps = {
email: string;
@@ -56,7 +55,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
*/
const isPublicDomain = async (emailAddress: string): Promise<boolean> => {
// Get metadata from storage
const publicEmailDomains = await storage.getItem('local:publicEmailDomains') as string[] ?? [];
const publicEmailDomains = await getItemWithFallback<string[]>('local:publicEmailDomains') ?? [];
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(`@${domain.toLowerCase()}`));
};
@@ -65,7 +64,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
*/
const isPrivateDomain = async (emailAddress: string): Promise<boolean> => {
// Get metadata from storage
const privateEmailDomains = await storage.getItem('local:privateEmailDomains') as string[] ?? [];
const privateEmailDomains = await getItemWithFallback<string[]>('local:privateEmailDomains') ?? [];
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(`@${domain.toLowerCase()}`));
};

View File

@@ -26,20 +26,6 @@ type EmailDomainFieldProps = {
onGenerateAlias?: () => void;
}
// Hardcoded public email domains (same as in AliasVault.Client)
const PUBLIC_EMAIL_DOMAINS = [
'spamok.com',
'solarflarecorp.com',
'spamok.nl',
'3060.nl',
'landmail.nl',
'asdasd.nl',
'spamok.de',
'spamok.com.ua',
'spamok.es',
'spamok.fr',
];
/**
* Email domain field component with domain chooser functionality.
* Allows users to select from private/public domains or enter custom email addresses.
@@ -61,17 +47,19 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const [localPart, setLocalPart] = useState('');
const [selectedDomain, setSelectedDomain] = useState('');
const [isPopupVisible, setIsPopupVisible] = useState(false);
const [publicEmailDomains, setPublicEmailDomains] = useState<string[]>([]);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
const popupRef = useRef<HTMLDivElement>(null);
// Get private email domains from vault metadata
// Get email domains from vault metadata
useEffect(() => {
/**
* Load private email domains from vault metadata, excluding hidden ones.
* Load email domains from vault metadata.
*/
const loadDomains = async (): Promise<void> => {
const metadata = await dbContext.getVaultMetadata();
setPublicEmailDomains(metadata?.publicEmailDomains ?? []);
setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
};
@@ -93,8 +81,8 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
if (!selectedDomain) {
if (showPrivateDomains && privateEmailDomains[0]) {
setSelectedDomain(privateEmailDomains[0]);
} else if (PUBLIC_EMAIL_DOMAINS[0]) {
setSelectedDomain(PUBLIC_EMAIL_DOMAINS[0]);
} else if (publicEmailDomains[0]) {
setSelectedDomain(publicEmailDomains[0]);
}
}
return;
@@ -106,7 +94,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setSelectedDomain(domain);
// Check if it's a known domain (public, private, or hidden private)
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
const isKnownDomain = publicEmailDomains.includes(domain) ||
privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
// Switch to domain chooser mode if domain is recognized
@@ -119,8 +107,8 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
if (!selectedDomain && !value.includes('@')) {
if (showPrivateDomains && privateEmailDomains[0]) {
setSelectedDomain(privateEmailDomains[0]);
} else if (PUBLIC_EMAIL_DOMAINS[0]) {
setSelectedDomain(PUBLIC_EMAIL_DOMAINS[0]);
} else if (publicEmailDomains[0]) {
setSelectedDomain(publicEmailDomains[0]);
}
}
}
@@ -142,7 +130,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
// Check if the domain is now recognized after private domains loaded
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
const isKnownDomain = publicEmailDomains.includes(domain) ||
privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
@@ -150,7 +138,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
if (isKnownDomain && isCustomDomain) {
setIsCustomDomain(false);
}
}, [privateEmailDomains, hiddenPrivateEmailDomains, value, isCustomDomain]);
}, [publicEmailDomains, privateEmailDomains, hiddenPrivateEmailDomains, value, isCustomDomain]);
// Handle local part changes
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -217,7 +205,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
// Switching to domain chooser mode
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
? privateEmailDomains[0]
: PUBLIC_EMAIL_DOMAINS[0];
: publicEmailDomains[0];
setSelectedDomain(defaultDomain);
// Only add domain if we have a local part
@@ -228,7 +216,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
onChange(`${value}@${defaultDomain}`);
}
}
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange, defaultToFreeText]);
}, [isCustomDomain, value, localPart, showPrivateDomains, publicEmailDomains, privateEmailDomains, onChange, defaultToFreeText]);
// Handle clicks outside the popup
useEffect(() => {
@@ -386,7 +374,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{t('credentials.publicEmailDescription')}
</p>
<div className="flex flex-wrap gap-2">
{PUBLIC_EMAIL_DOMAINS.map((domain) => (
{publicEmailDomains.map((domain) => (
<button
key={domain}
type="button"

View File

@@ -3,6 +3,7 @@ import { sendMessage } from 'webext-bridge/popup';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/core/models/metadata';
import SqliteClient from '@/utils/SqliteClient';
import { getItemWithFallback } from '@/utils/StorageUtility';
import type { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
import { storage } from '#imports';
@@ -175,12 +176,11 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
*/
const getVaultMetadata = useCallback(async () : Promise<VaultMetadata | null> => {
try {
const [publicEmailDomains, privateEmailDomains, hiddenPrivateEmailDomains, revision] = await Promise.all([
storage.getItem('local:publicEmailDomains') as Promise<string[] | null>,
storage.getItem('local:privateEmailDomains') as Promise<string[] | null>,
storage.getItem('local:hiddenPrivateEmailDomains') as Promise<string[] | null>,
storage.getItem('local:serverRevision') as Promise<number | null>
]);
// Use fallback for keys migrated from session: to local: in v0.26.0
const publicEmailDomains = await getItemWithFallback<string[]>('local:publicEmailDomains');
const privateEmailDomains = await getItemWithFallback<string[]>('local:privateEmailDomains');
const hiddenPrivateEmailDomains = await getItemWithFallback<string[]>('local:hiddenPrivateEmailDomains');
const revision = await storage.getItem('local:serverRevision') as number | null;
if (!publicEmailDomains && !privateEmailDomains) {
return null;

View File

@@ -6,12 +6,11 @@ import type { Attachment } from '@/utils/dist/core/models/vault';
import { FieldKey, FieldTypes, getSystemField, MAX_FIELD_HISTORY_RECORDS } from '@/utils/dist/core/models/vault';
import type { VaultVersion } from '@/utils/dist/core/vault';
import { VaultSqlGenerator, checkVersionCompatibility, extractVersionFromMigrationId } from '@/utils/dist/core/vault';
import { getItemWithFallback } from '@/utils/StorageUtility';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
import { t } from '@/i18n/StandaloneI18n';
import { storage } from '#imports';
/**
* Placeholder base64 image for credentials without a logo.
*/
@@ -756,9 +755,10 @@ export class SqliteClient {
* @returns The default email domain or null if no valid domain is found
*/
public async getDefaultEmailDomain(): Promise<string | null> {
const publicEmailDomains = await storage.getItem('local:publicEmailDomains') as string[] ?? [];
const privateEmailDomains = await storage.getItem('local:privateEmailDomains') as string[] ?? [];
const hiddenPrivateEmailDomains = await storage.getItem('local:hiddenPrivateEmailDomains') as string[] ?? [];
// Use fallback for keys migrated from session: to local: in v0.26.0
const publicEmailDomains = await getItemWithFallback<string[]>('local:publicEmailDomains') ?? [];
const privateEmailDomains = await getItemWithFallback<string[]>('local:privateEmailDomains') ?? [];
const hiddenPrivateEmailDomains = await getItemWithFallback<string[]>('local:hiddenPrivateEmailDomains') ?? [];
const defaultEmailDomain = this.getSetting('DefaultEmailDomain');

View File

@@ -0,0 +1,53 @@
import { storage } from 'wxt/utils/storage';
type StorageKey = `local:${string}` | `session:${string}`;
/**
* Storage keys that were migrated from session: to local: storage in v0.26.0 for offline mode support.
* This mapping enables backward compatibility for users upgrading from older versions where data
* was stored in session: storage. The fallback can be removed in v0.27.0+.
*
* Format: local key -> session fallback key
*/
const MIGRATED_STORAGE_KEYS: Record<string, StorageKey> = {
'local:publicEmailDomains': 'session:publicEmailDomains',
'local:privateEmailDomains': 'session:privateEmailDomains',
'local:hiddenPrivateEmailDomains': 'session:hiddenPrivateEmailDomains',
'local:encryptionKeyDerivationParams': 'session:encryptionKeyDerivationParams',
};
/**
* Get a storage item with fallback to the legacy session: storage location.
* This is used for keys that were migrated from session: to local: storage in v0.26.0.
*
* @param key The local: storage key to retrieve
* @returns The value from local: storage, or from session: storage as fallback, or null if not found
*
* @example
* // Instead of:
* const domains = await storage.getItem('local:publicEmailDomains');
*
* // Use:
* const domains = await getItemWithFallback('local:publicEmailDomains');
*
* @note This fallback can be removed in v0.27.0+ after users have had time to upgrade
*/
export async function getItemWithFallback<T>(key: StorageKey): Promise<T | null> {
// Try the current (local:) key first
let value = await storage.getItem(key) as T | null;
// If not found and this is a migrated key, try the fallback
if (value === null && key in MIGRATED_STORAGE_KEYS) {
const fallbackKey = MIGRATED_STORAGE_KEYS[key];
value = await storage.getItem(fallbackKey) as T | null;
// If found in fallback, migrate to new location for future use
if (value !== null) {
await storage.setItem(key, value);
// Remove the fallback key
await storage.removeItem(fallbackKey);
}
}
return value;
}