Compare commits

...

9 Commits

Author SHA1 Message Date
Leendert de Borst
0a70902d69 Bump version to 0.25.1 for mobile app (unaffected by 0.25.2 release) 2025-11-30 17:47:58 +01:00
Leendert de Borst
eee41df9a4 Bump version to 0.25.2 2025-11-30 17:30:32 +01:00
Leendert de Borst
d563d6d448 Improve browser extension vault cache (#1413) 2025-11-30 17:26:23 +01:00
Leendert de Borst
db1474397c Add cascade delete to MobileLoginRequests (#1415) 2025-11-30 15:38:49 +00:00
Leendert de Borst
e881f9486a Add parallel support to db-export command (#1415) 2025-11-30 15:12:55 +00:00
Leendert de Borst
645fd605e6 Update PasswordGenerator.test.ts (#1413) 2025-11-30 12:08:22 +00:00
Leendert de Borst
254f0a1212 Improve browser extension autofill suggestion performance (#1413) 2025-11-30 12:08:22 +00:00
Leendert de Borst
64d29ebcd4 Update admin users list to show correct amount of email claims (#1411) 2025-11-30 11:17:16 +00:00
Leendert de Borst
df0d74595f Bump version to 0.26.0-alpha 2025-11-28 20:16:39 +01:00
25 changed files with 1355 additions and 118 deletions

View File

@@ -1 +1 @@
1
2

View File

@@ -1 +1 @@
0.25.1
0.25.2

View File

@@ -2,7 +2,7 @@
"name": "aliasvault-browser-extension",
"description": "AliasVault Browser Extension",
"private": true,
"version": "0.25.1",
"version": "0.25.2",
"type": "module",
"scripts": {
"dev:chrome": "wxt -b chrome",

View File

@@ -463,7 +463,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2501900;
CURRENT_PROJECT_VERSION = 2502900;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -476,7 +476,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.25.1;
MARKETING_VERSION = 0.25.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -495,7 +495,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2501900;
CURRENT_PROJECT_VERSION = 2502900;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -508,7 +508,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.25.1;
MARKETING_VERSION = 0.25.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -532,7 +532,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2501900;
CURRENT_PROJECT_VERSION = 2502900;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -547,7 +547,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.25.1;
MARKETING_VERSION = 0.25.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -571,7 +571,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2501900;
CURRENT_PROJECT_VERSION = 2502900;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -586,7 +586,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.25.1;
MARKETING_VERSION = 0.25.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

@@ -9,7 +9,7 @@ import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardCl
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler';
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetFilteredCredentials, handleGetSearchCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
import { EncryptionKeyDerivationParams } from "@/utils/dist/shared/models/metadata";
@@ -28,6 +28,8 @@ export default defineBackground({
onMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', () => handleGetEncryptionKeyDerivationParams());
onMessage('GET_VAULT', () => handleGetVault());
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
onMessage('GET_FILTERED_CREDENTIALS', ({ data }) => handleGetFilteredCredentials(data as { currentUrl: string, pageTitle: string, matchingMode?: string }));
onMessage('GET_SEARCH_CREDENTIALS', ({ data }) => handleGetSearchCredentials(data as { searchTerm: string }));
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());

View File

@@ -4,12 +4,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { handleGetEncryptionKey } from '@/entrypoints/background/VaultMessageHandler';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
import {
PASSKEY_PROVIDER_ENABLED_KEY,
PASSKEY_DISABLED_SITES_KEY
} from '@/utils/Constants';
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
import type {

View File

@@ -18,6 +18,21 @@ import { WebApiService } from '@/utils/WebApiService';
import { t } from '@/i18n/StandaloneI18n';
/**
* Cache for the SqliteClient to avoid repeated decryption and initialization.
* The cached instance is the single source of truth for the in-memory vault.
*
* Cache Strategy:
* - Local mutations (createCredential, etc.): Work directly on cachedSqliteClient, no cache clearing
* - New vault from remote (login, sync): Clear cache by setting both to null
* - Logout/clear vault: Clear cache by setting both to null
*
* The cache is cleared by setting cachedSqliteClient and cachedVaultBlob to null directly
* in the functions that receive new vault data from external sources.
*/
let cachedSqliteClient: SqliteClient | null = null;
let cachedVaultBlob: string | null = null;
/**
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
*/
@@ -58,8 +73,6 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
hasPendingMigrations
};
} catch (error) {
console.error('Error checking pending migrations:', error);
// If it's a version incompatibility error, we need to handle it specially
if (error instanceof VaultVersionIncompatibleError) {
// Return the error so the UI can handle it appropriately (logout user)
@@ -92,6 +105,10 @@ export async function handleStoreVault(
// Store new encrypted vault in session storage.
await storage.setItem('session:encryptedVault', vaultRequest.vaultBlob);
// Clear cached client since we received a new vault blob from external source
cachedSqliteClient = null;
cachedVaultBlob = null;
/*
* For all other values, check if they have a value and store them in session storage if they do.
* Some updates, e.g. when mutating local database, these values will not be set.
@@ -155,7 +172,7 @@ export async function handleStoreEncryptionKeyDerivationParams(
*/
export async function handleSyncVault(
) : Promise<messageBoolResponse> {
const webApi = new WebApiService(() => {});
const webApi = new WebApiService();
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
@@ -175,6 +192,10 @@ export async function handleSyncVault(
{ key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
]);
// Clear cached client since we received a new vault blob from server
cachedSqliteClient = null;
cachedVaultBlob = null;
}
return { success: true };
@@ -240,6 +261,10 @@ export function handleClearVault(
'session:vaultRevisionNumber'
]);
// Clear cached client since vault was cleared
cachedSqliteClient = null;
cachedVaultBlob = null;
return { success: true };
}
@@ -264,6 +289,100 @@ export async function handleGetCredentials(
}
}
/**
* Get credentials filtered by URL and page title for autofill performance optimization.
* Filters credentials in the background script before sending to reduce message payload size.
* Critical for large vaults (1000+ credentials) to avoid multi-second delays.
*
* @param message - Filtering parameters: currentUrl, pageTitle, matchingMode
*/
export async function handleGetFilteredCredentials(
message: { currentUrl: string, pageTitle: string, matchingMode?: string }
) : Promise<messageCredentialsResponse> {
const encryptionKey = await handleGetEncryptionKey();
if (!encryptionKey) {
return { success: false, error: await t('common.errors.vaultIsLocked') };
}
try {
const sqliteClient = await createVaultSqliteClient();
const allCredentials = sqliteClient.getAllCredentials();
const { filterCredentials, AutofillMatchingMode } = await import('@/utils/credentialMatcher/CredentialMatcher');
// Parse matching mode from string
let matchingMode = AutofillMatchingMode.DEFAULT;
if (message.matchingMode) {
matchingMode = message.matchingMode as typeof AutofillMatchingMode[keyof typeof AutofillMatchingMode];
}
// Filter credentials in background to reduce payload size (~95% reduction)
const filteredCredentials = filterCredentials(
allCredentials,
message.currentUrl,
message.pageTitle,
matchingMode
);
return { success: true, credentials: filteredCredentials };
} catch (error) {
console.error('Error getting filtered credentials:', error);
return { success: false, error: await t('common.errors.unknownError') };
}
}
/**
* Get credentials filtered by text search query.
* Searches across entire vault (service name, username, email, URL) and returns matches.
*
* @param message - Search parameters: searchTerm
*/
export async function handleGetSearchCredentials(
message: { searchTerm: string }
) : Promise<messageCredentialsResponse> {
const encryptionKey = await handleGetEncryptionKey();
if (!encryptionKey) {
return { success: false, error: await t('common.errors.vaultIsLocked') };
}
try {
const sqliteClient = await createVaultSqliteClient();
const allCredentials = sqliteClient.getAllCredentials();
// If search term is empty, return empty array
if (!message.searchTerm || message.searchTerm.trim() === '') {
return { success: true, credentials: [] };
}
const searchTerm = message.searchTerm.toLowerCase().trim();
// Filter credentials by search term across multiple fields
const searchResults = allCredentials.filter(cred => {
const searchableFields = [
cred.ServiceName?.toLowerCase(),
cred.Username?.toLowerCase(),
cred.Alias?.Email?.toLowerCase(),
cred.ServiceUrl?.toLowerCase()
];
return searchableFields.some(field => field?.includes(searchTerm));
}).sort((a, b) => {
// Sort by service name, then username
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
if (serviceNameComparison !== 0) {
return serviceNameComparison;
}
return (a.Username ?? '').localeCompare(b.Username ?? '');
});
return { success: true, credentials: searchResults };
} catch (error) {
console.error('Error searching credentials:', error);
return { success: false, error: await t('common.errors.unknownError') };
}
}
/**
* Create an identity.
*/
@@ -405,13 +524,11 @@ export async function handleUploadVault(
message: any
) : Promise<messageVaultUploadResponse> {
try {
// Store the new vault blob in session storage.
// Persist the current updated vault blob in session storage.
await storage.setItem('session:encryptedVault', message.vaultBlob);
// Create new sqlite client which will use the new vault blob.
const sqliteClient = await createVaultSqliteClient();
// Upload the new vault to the server.
const sqliteClient = await createVaultSqliteClient();
const response = await uploadNewVaultToServer(sqliteClient);
return { success: true, status: response.status, newRevisionNumber: response.newRevisionNumber };
} catch (error) {
@@ -486,10 +603,17 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
encryptionKey
);
// Update storage with the newly encrypted vault (serialized from current in-memory state)
await storage.setItems([
{ key: 'session:encryptedVault', value: encryptedVault }
]);
/*
* Update cached vault blob to match the new encrypted version
* This prevents unnecessary cache invalidation since the in-memory sqliteClient is already up to date
*/
cachedVaultBlob = encryptedVault;
// Get metadata from storage
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
@@ -510,7 +634,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
encryptionPublicKey: '',
};
const webApi = new WebApiService(() => {});
const webApi = new WebApiService();
const response = await webApi.post<Vault, VaultPostResponse>('Vault', newVault);
// Check if response is successful (.status === 0)
@@ -525,6 +649,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
/**
* Create a new sqlite client for the stored vault.
* Uses a cache to avoid repeated decryption and initialization for read operations.
*/
async function createVaultSqliteClient() : Promise<SqliteClient> {
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
@@ -533,15 +658,24 @@ async function createVaultSqliteClient() : Promise<SqliteClient> {
throw new Error(await t('common.errors.unknownError'));
}
// Decrypt the vault.
// Check if we have a valid cached client
if (cachedSqliteClient && cachedVaultBlob === encryptedVault) {
return cachedSqliteClient;
}
// Decrypt the vault
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
encryptedVault,
encryptionKey
);
// Initialize the SQLite client with the decrypted vault.
// Initialize the SQLite client with the decrypted vault
const sqliteClient = new SqliteClient();
await sqliteClient.initializeFromBase64(decryptedVault);
// Cache the client and vault blob
cachedSqliteClient = sqliteClient;
cachedVaultBlob = encryptedVault;
return sqliteClient;
}

View File

@@ -1,9 +1,9 @@
import { sendMessage } from 'webext-bridge/content-script';
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher';
import { fillCredential } from '@/entrypoints/contentScript/Form';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
import type { Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator';
@@ -49,7 +49,14 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen
document.addEventListener('keydown', handleEnterKey);
(async () : Promise<void> => {
const response = await sendMessage('GET_CREDENTIALS', { }, 'background') as CredentialsResponse;
// Load autofill matching mode setting to send to background for filtering
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
const response = await sendMessage('GET_FILTERED_CREDENTIALS', {
currentUrl: window.location.href,
pageTitle: document.title,
matchingMode: matchingMode
}, 'background') as CredentialsResponse;
if (response.success) {
await createAutofillPopup(input, response.credentials, container);
@@ -182,22 +189,12 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
credentialList.className = 'av-credential-list';
popup.appendChild(credentialList);
// Add initial credentials
// Add initial credentials (already filtered by background script for performance)
if (!credentials) {
credentials = [];
}
// Load autofill matching mode setting
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
const filteredCredentials = filterCredentials(
credentials,
window.location.href,
document.title,
matchingMode
);
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
updatePopupContent(credentials, credentialList, input, rootContainer, noMatchesText);
// Add divider
const divider = document.createElement('div');
@@ -549,62 +546,41 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
}
/**
* Handle popup search input by filtering credentials based on the search term.
* Handle popup search input - searches entire vault when user types.
* When empty, shows the initially URL-filtered credentials.
* When user types, searches ALL credentials in vault (not just the pre-filtered set).
*
* @param searchInput - The search input element
* @param initialCredentials - The initially URL-filtered credentials to show when search is empty
* @param rootContainer - The root container element
* @param searchTimeout - Timeout for debouncing search
* @param credentialList - The credential list element to update
* @param input - The input field that triggered the popup
* @param noMatchesText - Text to show when no matches found
*/
async function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
async function handleSearchInput(searchInput: HTMLInputElement, initialCredentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
const searchTerm = searchInput.value.toLowerCase();
// Ensure we have unique credentials
const uniqueCredentials = Array.from(new Map(credentials.map(cred => [cred.Id, cred])).values());
let filteredCredentials;
const searchTerm = searchInput.value.trim();
if (searchTerm === '') {
// Load autofill matching mode setting
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
// If search is empty, use original URL-based filtering
filteredCredentials = filterCredentials(
uniqueCredentials,
window.location.href,
document.title,
matchingMode
).sort((a, b) => {
// First compare by service name
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
if (serviceNameComparison !== 0) {
return serviceNameComparison;
}
// If service names are equal, compare by username/nickname
return (a.Username ?? '').localeCompare(b.Username ?? '');
});
// If search is empty, show the initially URL-filtered credentials
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
} else {
// Otherwise filter based on search term
filteredCredentials = uniqueCredentials.filter(cred => {
const searchableFields = [
cred.ServiceName?.toLowerCase(),
cred.Username?.toLowerCase(),
cred.Alias?.Email?.toLowerCase(),
cred.ServiceUrl?.toLowerCase()
];
return searchableFields.some(field => field?.includes(searchTerm));
}).sort((a, b) => {
// First compare by service name
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
if (serviceNameComparison !== 0) {
return serviceNameComparison;
}
// Search in full vault with search term
const response = await sendMessage('GET_SEARCH_CREDENTIALS', {
searchTerm: searchTerm
}, 'background') as CredentialsResponse;
// If service names are equal, compare by username/nickname
return (a.Username ?? '').localeCompare(b.Username ?? '');
});
if (response.success && response.credentials) {
updatePopupContent(response.credentials, credentialList, input, rootContainer, noMatchesText);
} else {
// On error, fallback to showing initial filtered credentials
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
}
}
// Update popup content with filtered results
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
}
/**

View File

@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
import Button from '@/entrypoints/popup/components/Button';
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
@@ -12,6 +11,7 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
import type { GetRequest, PasskeyGetCredentialResponse, PendingPasskeyGetRequest, StoredPasskeyRecord } from '@/utils/passkey/types';

View File

@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
import Alert from '@/entrypoints/popup/components/Alert';
import Button from '@/entrypoints/popup/components/Button';
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
@@ -16,6 +15,7 @@ import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedi
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
import type { Passkey } from '@/utils/dist/shared/models/vault';
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import {
@@ -10,6 +9,7 @@ import {
TEMPORARY_DISABLED_SITES_KEY,
AUTOFILL_MATCHING_MODE_KEY
} from '@/utils/Constants';
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
import { storage, browser } from "#imports";

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import {
PASSKEY_PROVIDER_ENABLED_KEY,
PASSKEY_DISABLED_SITES_KEY
} from '@/utils/Constants';
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
import { storage, browser } from "#imports";

View File

@@ -6,7 +6,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.25.1';
public static readonly VERSION = '0.25.2';
/**
* The API version to send to the server (base semver without stage suffixes).

View File

@@ -1,9 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { filterCredentials } from '@/utils/credentialMatcher/CredentialMatcher';
import type { Credential } from '@/utils/dist/shared/models/vault';
import { filterCredentials } from '../CredentialMatcher';
describe('CredentialMatcher - Credential URL Matching', () => {
let testCredentials: Credential[];

View File

@@ -20,7 +20,7 @@ export default defineConfig({
return {
name: "AliasVault",
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
version: "0.25.1",
version: "0.25.2",
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},

View File

@@ -218,11 +218,7 @@ else
v.RevisionNumber,
CredentialCount = v.CredentialsCount,
}),
EmailClaims = u.EmailClaims.Select(ec => new
{
ec.CreatedAt,
ec.Address
}),
EmailClaimCount = u.EmailClaims.Count(),
})
.ToListAsync(cancellationToken);
@@ -247,7 +243,7 @@ else
IsInactive = isInactive,
VaultCount = user.Vaults.Count(),
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
EmailClaimCount = user.EmailClaims.Count(),
EmailClaimCount = user.EmailClaimCount,
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
};
}).ToList();

View File

@@ -273,5 +273,12 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
.WithMany(c => c.EncryptionKeys)
.HasForeignKey(l => l.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure MobileLoginRequest - AliasVaultUser relationship
modelBuilder.Entity<MobileLoginRequest>()
.HasOne(m => m.User)
.WithMany()
.HasForeignKey(m => m.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,985 @@
// <auto-generated />
using System;
using AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AliasServerDb.Migrations
{
[DbContext(typeof(AliasServerDbContext))]
[Migration("20251130152655_ConfigureMobileLoginRequestCascadeDelete")]
partial class ConfigureMobileLoginRequestCascadeDelete
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("NormalizedName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("NormalizedName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<bool>("Blocked")
.HasColumnType("boolean");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<int>("EmailsReceived")
.HasColumnType("integer");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<int>("MaxEmailAgeDays")
.HasColumnType("integer");
b.Property<int>("MaxEmails")
.HasColumnType("integer");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text");
b.Property<DateTime>("PasswordChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("timestamp with time zone");
b.Property<string>("IpAddress")
.HasMaxLength(45)
.HasColumnType("character varying(45)");
b.Property<string>("PreviousTokenValue")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AdditionalInfo")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Client")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Country")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DeviceType")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("EventType")
.HasColumnType("integer");
b.Property<int?>("FailureReason")
.HasColumnType("integer");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean");
b.Property<bool>("IsSuspiciousActivity")
.HasColumnType("boolean");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("RequestPath")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex(new[] { "EventType" }, "IX_EventType");
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
.IsDescending(false, false, true);
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
.IsDescending(false, true);
b.ToTable("AuthLogs");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateSystem")
.HasColumnType("timestamp with time zone");
b.Property<string>("EncryptedSymmetricKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("From")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MessageHtml")
.HasColumnType("text");
b.Property<string>("MessagePlain")
.HasColumnType("text");
b.Property<string>("MessagePreview")
.HasColumnType("text");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("PushNotificationSent")
.HasColumnType("boolean");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<string>("To")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserEncryptionKeyId")
.HasMaxLength(255)
.HasColumnType("uuid");
b.Property<bool>("Visible")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("UserEncryptionKeyId");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("bytea");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("EmailId")
.HasColumnType("integer");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Filesize")
.HasColumnType("integer");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Application")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Level")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("LogEvent")
.IsRequired()
.HasColumnType("text")
.HasColumnName("LogEvent");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SourceContext")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("TimeStamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Application");
b.HasIndex("TimeStamp");
b.ToTable("Logs", (string)null);
});
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime?>("ClearedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ClientIpAddress")
.HasColumnType("text");
b.Property<string>("ClientPublicKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EncryptedDecryptionKey")
.HasColumnType("text");
b.Property<DateTime?>("FulfilledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("MobileIpAddress")
.HasColumnType("text");
b.Property<DateTime?>("RetrievedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress");
b.HasIndex(new[] { "CreatedAt" }, "IX_CreatedAt");
b.HasIndex(new[] { "MobileIpAddress" }, "IX_MobileIpAddress");
b.HasIndex(new[] { "RetrievedAt", "ClearedAt", "FulfilledAt" }, "IX_RetrievedAt_ClearedAt_FulfilledAt");
b.HasIndex(new[] { "UserId" }, "IX_UserId");
b.ToTable("MobileLoginRequests");
});
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key");
b.ToTable("ServerSettings");
});
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<TimeOnly?>("EndTime")
.HasColumnType("time without time zone");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<bool>("IsOnDemand")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("RunDate")
.HasColumnType("timestamp with time zone");
b.Property<TimeOnly>("StartTime")
.HasColumnType("time without time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("TaskRunnerJobs");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AddressDomain")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AddressLocal")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Disabled")
.HasColumnType("boolean");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("Address")
.IsUnique();
b.HasIndex("UserId", "Disabled");
b.ToTable("UserEmailClaims");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsPrimary")
.HasColumnType("boolean");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserEncryptionKeys");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Client")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("CredentialsCount")
.HasColumnType("integer");
b.Property<int>("EmailClaimsCount")
.HasColumnType("integer");
b.Property<string>("EncryptionSettings")
.IsRequired()
.HasColumnType("text");
b.Property<string>("EncryptionType")
.IsRequired()
.HasColumnType("text");
b.Property<int>("FileSize")
.HasColumnType("integer");
b.Property<long>("RevisionNumber")
.HasColumnType("bigint");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("VaultBlob")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Verifier")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("FriendlyName")
.HasColumnType("text");
b.Property<string>("Xml")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
.WithMany("Emails")
.HasForeignKey("UserEncryptionKeyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("EncryptionKey");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EmailClaims")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EncryptionKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("Vaults")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Navigation("EmailClaims");
b.Navigation("EncryptionKeys");
b.Navigation("Vaults");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Navigation("Emails");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,42 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class ConfigureMobileLoginRequestCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
table: "MobileLoginRequests");
migrationBuilder.AddForeignKey(
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
table: "MobileLoginRequests",
column: "UserId",
principalTable: "AliasVaultUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
table: "MobileLoginRequests");
migrationBuilder.AddForeignKey(
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
table: "MobileLoginRequests",
column: "UserId",
principalTable: "AliasVaultUsers",
principalColumn: "Id");
}
}
}

View File

@@ -920,7 +920,8 @@ namespace AliasServerDb.Migrations
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId");
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("User");
});

View File

@@ -30,7 +30,7 @@ public static class AppInfo
/// <summary>
/// Gets the patch version number.
/// </summary>
public const int VersionPatch = 1;
public const int VersionPatch = 2;
/// <summary>
/// Gets the version stage (e.g., "", "-alpha", "-beta", "-rc").

View File

@@ -0,0 +1 @@
- Improve autofill performance

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# @version 0.25.0
# @version 0.25.2
# Repository information used for downloading files and images from GitHub
REPO_OWNER="aliasvault"
@@ -72,9 +72,10 @@ show_usage() {
printf " configure-dev-db Enable/disable development database (for local development only)\n"
printf "\n"
printf "Options:\n"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts\n"
printf " --dev Target development database for db import/export operations"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts\n"
printf " --dev Target development database for db import/export operations\n"
printf " --parallel=N Use pigz with N threads for faster compression (default: off, max: 32)\n"
printf "\n"
}
@@ -98,6 +99,7 @@ parse_args() {
FORCE_YES=false
COMMAND_ARG=""
DEV_DB=false
PARALLEL_JOBS=0 # 0 = use standard gzip, >0 = use pigz with N threads
if [ $# -eq 0 ]; then
show_usage
@@ -228,6 +230,14 @@ parse_args() {
DEV_DB=true
shift
;;
--parallel=*)
PARALLEL_JOBS="${1#*=}"
if ! [[ "$PARALLEL_JOBS" =~ ^[0-9]+$ ]] || [ "$PARALLEL_JOBS" -lt 1 ] || [ "$PARALLEL_JOBS" -gt 32 ]; then
echo "Error: Invalid --parallel value '$PARALLEL_JOBS'. Must be a number between 1 and 32"
exit 1
fi
shift
;;
*)
echo "Unknown option: $1"
show_usage
@@ -2816,18 +2826,30 @@ handle_db_export() {
# Check if output redirection is present
if [ -t 1 ]; then
printf "Usage: ./install.sh db-export [--dev] > backup.sql.gz\n" >&2
printf "Usage: ./install.sh db-export [OPTIONS] > backup.sql.gz\n" >&2
printf "\n" >&2
printf "Options:\n" >&2
printf " --dev Export from development database\n" >&2
printf " --dev Export from development database\n" >&2
printf " --parallel=N Use pigz with N threads for parallel compression (max: 32)\n" >&2
printf "\n" >&2
printf "Examples:\n" >&2
printf " ./install.sh db-export > my_backup_$(date +%Y%m%d).sql.gz\n" >&2
printf " ./install.sh db-export --dev > my_dev_backup_$(date +%Y%m%d).sql.gz\n" >&2
printf "Compression:\n" >&2
printf " Default (no --parallel) Uses standard gzip (slowest, lowest CPU usage)\n" >&2
printf " --parallel=X Uses pigz with X threads (~2x faster, good for production)\n" >&2
printf "\n" >&2
printf "Note: Parallel compression runs at lowest priority (nice/ionice) to minimize\n" >&2
printf " impact on production.\n" >&2
printf "\n" >&2
exit 1
fi
# Create temporary file for export
temp_export_file=$(mktemp)
trap 'rm -f "$temp_export_file"' EXIT INT TERM
# Start timing
export_start_time=$(date +%s)
# Determine docker compose command based on dev/prod
if [ "$DEV_DB" = true ]; then
# Check if dev containers are running
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps postgres-dev --quiet 2>/dev/null | grep -q .; then
@@ -2841,8 +2863,8 @@ handle_db_export() {
exit 1
fi
printf "${CYAN}> Exporting development database...${NC}\n" >&2
docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec postgres-dev pg_dump -U aliasvault aliasvault | gzip
DOCKER_CMD="docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev"
DB_TYPE="development"
else
# Production database export logic
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
@@ -2855,12 +2877,67 @@ handle_db_export() {
exit 1
fi
printf "${CYAN}> Exporting production database...${NC}\n" >&2
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip
DOCKER_CMD="docker compose exec -T postgres"
DB_TYPE="production"
fi
if [ $? -eq 0 ]; then
# Execute export based on parallel setting
if [ "$PARALLEL_JOBS" -gt 0 ]; then
printf "${CYAN}> Exporting ${DB_TYPE} database (with ${PARALLEL_JOBS}-thread parallel compression)...${NC}\n" >&2
# Use pigz for parallel compression
$DOCKER_CMD bash -c "
# Install pigz if not available (for parallel gzip)
if ! command -v pigz >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache pigz >/dev/null 2>&1 || true
elif command -v apt-get >/dev/null 2>&1; then
apt-get update >/dev/null 2>&1 && apt-get install -y pigz >/dev/null 2>&1 || true
fi
fi
# Dump with pigz parallel compression (or fallback to gzip -1 if pigz install failed)
# Use nice (lowest CPU priority) and ionice (lowest I/O priority) to minimize impact
if command -v pigz >/dev/null 2>&1; then
ionice -c 3 nice -n 19 pg_dump -U aliasvault aliasvault | ionice -c 3 nice -n 19 pigz -1 -p ${PARALLEL_JOBS} 2>/dev/null || \
nice -n 19 pg_dump -U aliasvault aliasvault | nice -n 19 pigz -1 -p ${PARALLEL_JOBS}
else
ionice -c 3 nice -n 19 pg_dump -U aliasvault aliasvault | ionice -c 3 nice -n 19 gzip -1 2>/dev/null || \
nice -n 19 pg_dump -U aliasvault aliasvault | nice -n 19 gzip -1
fi
" > "$temp_export_file" 2>/dev/null
export_status=$?
else
# Default: standard gzip (backwards compatible)
printf "${CYAN}> Exporting ${DB_TYPE} database (standard compression)...${NC}\n" >&2
$DOCKER_CMD nice -n 19 pg_dump -U aliasvault aliasvault | gzip -1 > "$temp_export_file"
export_status=$?
fi
# End timing
export_end_time=$(date +%s)
export_duration=$((export_end_time - export_start_time))
# Get filesize
if [ -f "$temp_export_file" ]; then
export_filesize=$(wc -c < "$temp_export_file")
export_filesize_mb=$(awk "BEGIN {printf \"%.2f\", $export_filesize/1024/1024}")
fi
if [ $export_status -eq 0 ]; then
# Output the file to stdout
cat "$temp_export_file"
printf "${GREEN}> Database exported successfully.${NC}\n" >&2
printf "${CYAN}> Export format: SQL (.sql.gz)${NC}\n" >&2
if [ "$PARALLEL_JOBS" -gt 0 ]; then
printf "${CYAN}> Compression: pigz with ${PARALLEL_JOBS} threads${NC}\n" >&2
else
printf "${CYAN}> Compression: gzip (standard)${NC}\n" >&2
fi
printf "${CYAN}> Export duration: ${export_duration}s${NC}\n" >&2
if [ -n "$export_filesize_mb" ]; then
printf "${CYAN}> Export filesize: ${export_filesize_mb} MB (compressed)${NC}\n" >&2
fi
else
printf "${RED}> Failed to export database.${NC}\n" >&2
exit 1
@@ -2892,9 +2969,9 @@ handle_db_import() {
printf " --dev Import to development database\n"
printf "\n"
printf "Examples:\n"
printf " ./install.sh db-import < backup.sql.gz # Import gzipped backup\n"
printf " ./install.sh db-import < backup.sql # Import plain SQL backup\n"
printf " ./install.sh db-import --dev < backup.sql # Import to dev database\n"
printf " ./install.sh db-import < backup.sql.gz # Import gzipped SQL (standard)\n"
printf " ./install.sh db-import < backup.sql # Import plain SQL\n"
printf " ./install.sh db-import --dev < backup.sql.gz # Import to dev database\n"
exit 1
fi
@@ -2951,8 +3028,16 @@ handle_db_import() {
cat <&3 > "$temp_file" # Read from fd 3 instead of stdin
exec 3<&- # Close fd 3
# Detect if the file is gzipped or plain SQL
# Get input filesize
if [ -f "$temp_file" ]; then
import_filesize=$(wc -c < "$temp_file")
import_filesize_mb=$(awk "BEGIN {printf \"%.2f\", $import_filesize/1024/1024}")
printf "${CYAN}> Input file size: ${import_filesize_mb} MB${NC}\n"
fi
# Detect file format
is_gzipped=false
if gzip -t "$temp_file" 2>/dev/null; then
is_gzipped=true
printf "${CYAN}> Detected gzipped SQL backup${NC}\n"
@@ -2966,6 +3051,9 @@ handle_db_import() {
fi
fi
# Start timing
import_start_time=$(date +%s)
if [ "$DEV_DB" = true ]; then
if [ "$VERBOSE" = true ]; then
docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
@@ -3009,10 +3097,16 @@ handle_db_import() {
fi
import_status=$?
# End timing
import_end_time=$(date +%s)
import_duration=$((import_end_time - import_start_time))
rm "$temp_file"
if [ $import_status -eq 0 ]; then
printf "${GREEN}> Database imported successfully.${NC}\n"
printf "${CYAN}> Import duration: ${import_duration}s${NC}\n"
if [ "$DEV_DB" != true ]; then
printf "${CYAN}> Starting services...${NC}\n"
if [ "$VERBOSE" = true ]; then

View File

@@ -131,7 +131,7 @@ describe('PasswordGenerator', () => {
expect(password).not.toMatch(/[Ss5]/);
expect(password).not.toMatch(/[Bb8]/);
expect(password).not.toMatch(/[Gg6]/);
expect(password).not.toMatch(/[[\]{}()<>]/);
expect(password).not.toMatch(/[\\[\\]{}()<>]/);
expect(password).not.toMatch(/['"`]/);
expect(password).not.toMatch(/[;:,.]/);
expect(password).not.toMatch(/[_-]/);