Compare commits

..

18 Commits

Author SHA1 Message Date
Leendert de Borst
83d9eadeea Bump version to 0.19.2 (#943) 2025-06-19 15:08:19 +02:00
Leendert de Borst
1cdd8f456e Make admin redirects work with custom ports through nginx docker (#940) 2025-06-19 11:52:43 +02:00
Leendert de Borst
395f881bd0 Bump version to 0.19.1 (#938) 2025-06-18 13:49:13 +02:00
Leendert de Borst
293ae102c5 Update history handling (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
8f5852bb86 Optimize load and persist flow (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
9ccaff74cd Update imports (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
ee6b40dd3d Refactor navigation logic from Home.tsx to NavigationContext (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
3ca4c0a78d Update icons folder casing (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
b246def212 Refactor persist logic to protect data at rest (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
1eecb8be38 Clear persisted form values if time has expired (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
9a7fbe7d2a Add form persist and restore logic (#935) 2025-06-18 13:30:14 +02:00
Leendert de Borst
7776fb6d82 Remember last visited page in browser extension and navigate back on reopen (#928) 2025-06-18 13:30:14 +02:00
Leendert de Borst
0eebaddf04 Move notes to bottom for view mode in mobile app and browser extension (#933) 2025-06-17 19:39:25 +02:00
Leendert de Borst
8b145e66b5 Only show email preview if email is supported by AliasVault public or private (#928) 2025-06-17 19:39:16 +02:00
Leendert de Borst
4e3c992c24 Update ErrorVaultDecrypt.razor typo (#928) 2025-06-17 19:39:16 +02:00
Leendert de Borst
65944b1523 Fix toast text color on dark mode (#931) 2025-06-17 19:39:07 +02:00
Leendert de Borst
d05114fddc Make view details and edit buttons work in iOS autofill popup (#931) 2025-06-17 19:39:07 +02:00
Leendert de Borst
8e0fef4b16 Add x-forwarded-prefix header to admin to support running on non-default ports (#929) 2025-06-17 19:38:56 +02:00
35 changed files with 618 additions and 183 deletions

View File

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

View File

@@ -447,7 +447,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -460,7 +460,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -479,7 +479,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -492,7 +492,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -515,7 +515,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -530,7 +530,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -554,7 +554,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

@@ -2,7 +2,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
@@ -30,6 +30,10 @@ export default defineBackground({
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));
onMessage('GET_PERSISTED_FORM_VALUES', () => handleGetPersistedFormValues());
onMessage('CLEAR_PERSISTED_FORM_VALUES', () => handleClearPersistedFormValues());
// Setup context menus
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
if (isContextMenuEnabled) {

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { setupContextMenus } from './ContextMenu';
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
import { browser } from '#imports';

View File

@@ -306,6 +306,56 @@ export async function handleUploadVault(
}
}
/**
* Handle persisting form values to storage.
* Data is encrypted using the derived key for additional security.
*/
export async function handlePersistFormValues(data: any): Promise<void> {
const derivedKey = await storage.getItem('session:derivedKey') as string;
if (!derivedKey) {
throw new Error('No derived key available for encryption');
}
// Always stringify the data properly
const serializedData = JSON.stringify(data);
const encryptedData = await EncryptionUtility.symmetricEncrypt(
serializedData,
derivedKey
);
await storage.setItem('session:persistedFormValues', encryptedData);
}
/**
* Handle retrieving persisted form values from storage.
* Data is decrypted using the derived key.
*/
export async function handleGetPersistedFormValues(): Promise<any | null> {
const derivedKey = await storage.getItem('session:derivedKey') as string;
const encryptedData = await storage.getItem('session:persistedFormValues') as string | null;
if (!encryptedData || !derivedKey) {
return null;
}
try {
const decryptedData = await EncryptionUtility.symmetricDecrypt(
encryptedData,
derivedKey
);
return JSON.parse(decryptedData);
} catch (error) {
console.error('Failed to decrypt or parse persisted form values:', error);
return null;
}
}
/**
* Handle clearing persisted form values from storage.
*/
export async function handleClearPersistedFormValues(): Promise<void> {
await storage.removeItem('session:persistedFormValues');
}
/**
* Upload a new version of the vault to the server using the provided sqlite client.
*/

View File

@@ -8,6 +8,7 @@ import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
@@ -15,10 +16,14 @@ import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
import Home from '@/entrypoints/popup/pages/Home';
import Login from '@/entrypoints/popup/pages/Login';
import Logout from '@/entrypoints/popup/pages/Logout';
import Settings from '@/entrypoints/popup/pages/Settings';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import '@/entrypoints/popup/style.css';
/**
@@ -44,6 +49,9 @@ const App: React.FC = () => {
// Add these route configurations
const routes: RouteConfig[] = [
{ path: '/', element: <Home />, showBackButton: false },
{ path: '/login', element: <Login />, showBackButton: false },
{ path: '/unlock', element: <Unlock />, showBackButton: false },
{ path: '/unlock-success', element: <UnlockSuccess onClose={() => window.location.search = ''} />, showBackButton: false },
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: 'Add credential' },
@@ -74,44 +82,45 @@ const App: React.FC = () => {
return (
<Router>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<NavigationProvider>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<GlobalStateChangeHandler />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<GlobalStateChangeHandler />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
</div>
</main>
<BottomNav />
</div>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
</div>
</main>
<BottomNav />
</div>
</NavigationProvider>
</Router>
);
};

View File

@@ -23,6 +23,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
const [lastEmailId, setLastEmailId] = useState<number>(0);
const [isSpamOk, setIsSpamOk] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
const webApi = useWebApi();
const dbContext = useDb();
@@ -35,6 +36,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
};
/**
* Checks if the email is a private domain.
*/
const isPrivateDomain = async (emailAddress: string): Promise<boolean> => {
// Get metadata from storage
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] ?? [];
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
};
useEffect(() => {
/**
* Loads the latest emails from the server and decrypts them locally if needed.
@@ -43,7 +53,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
try {
setError(null);
const isPublic = await isPublicDomain(email);
const isPrivate = await isPrivateDomain(email);
const isSupported = isPublic || isPrivate;
setIsSpamOk(isPublic);
setIsSupportedDomain(isSupported);
if (!isSupported) {
return;
}
if (isPublic) {
// For public domains (SpamOK), use the SpamOK API directly
@@ -73,7 +91,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
}
setEmails(latestMails);
} else {
} else if (isPrivate) {
// For private domains, use existing encrypted email logic
try {
/**
@@ -134,6 +152,11 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
return () : void => clearInterval(interval);
}, [email, loading, webApi, dbContext]);
// Don't render anything if the domain is not supported
if (!isSupportedDomain) {
return null;
}
if (error) {
return (
<div className="text-gray-500 dark:text-gray-400 mb-4">

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { HeaderIcon, HeaderIconType } from './icons/HeaderIcons';
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
type HeaderButtonProps = {
onClick: () => void;

View File

@@ -18,9 +18,13 @@ const BottomNav: React.FC = () => {
// Add effect to update currentTab based on route
useEffect(() => {
const path = location.pathname.substring(1) as TabName;
if (['credentials', 'emails', 'settings'].includes(path)) {
setCurrentTab(path);
const path = location.pathname.substring(1); // Remove leading slash
const tabNames: TabName[] = ['credentials', 'emails', 'settings'];
// Find the first tab name that matches the start of the path
const matchingTab = tabNames.find(tab => path === tab || path.startsWith(`${tab}/`));
if (matchingTab) {
setCurrentTab(matchingTab);
}
}, [location]);

View File

@@ -0,0 +1,195 @@
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { storage } from '#imports';
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
type NavigationHistoryEntry = {
pathname: string;
search: string;
hash: string;
};
type NavigationContextType = {
storeCurrentPage: () => Promise<void>;
restoreLastPage: () => Promise<void>;
isFullyInitialized: boolean;
requiresAuth: boolean;
};
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
/**
* Navigation provider component that handles storing and restoring the last visited page,
* as well as managing initialization and auth state redirects.
*/
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
const [isInitialized, setIsInitialized] = useState(false);
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
const { setIsInitialLoading } = useLoading();
// Auth and DB state
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
const { dbInitialized, dbAvailable } = useDb();
// Derived state
const isFullyInitialized = authInitialized && dbInitialized;
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable || isInlineUnlockMode);
/**
* Store the current page path, timestamp, and navigation history in storage.
*/
const storeCurrentPage = useCallback(async (): Promise<void> => {
// Pages that are not allowed to be stored as these are auth conditional pages.
const notAllowedPaths = ['/', '/login', '/unlock', '/unlock-success', '/auth-settings'];
// Only store the page if we're fully initialized and don't need auth
if (isFullyInitialized && !requiresAuth && !notAllowedPaths.includes(location.pathname)) {
// Split the path into segments and build up the history
const segments = location.pathname.split('/').filter(Boolean);
const historyEntries: NavigationHistoryEntry[] = [];
let currentPath = '';
for (const segment of segments) {
currentPath += '/' + segment;
historyEntries.push({
pathname: currentPath,
search: location.search,
hash: location.hash,
});
}
await Promise.all([
storage.setItem(LAST_VISITED_PAGE_KEY, location.pathname),
storage.setItem(LAST_VISITED_TIME_KEY, Date.now()),
storage.setItem(NAVIGATION_HISTORY_KEY, historyEntries),
]);
}
}, [location, isFullyInitialized, requiresAuth]);
/**
* Restore the last visited page and navigation history if it was visited within the memory duration.
*/
const restoreLastPage = useCallback(async (): Promise<void> => {
// Only restore if we're fully initialized and don't need auth
if (!isFullyInitialized || requiresAuth) {
return;
}
const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
storage.getItem(LAST_VISITED_PAGE_KEY) as Promise<string>,
storage.getItem(LAST_VISITED_TIME_KEY) as Promise<number>,
storage.getItem(NAVIGATION_HISTORY_KEY) as Promise<NavigationHistoryEntry[]>,
]);
if (lastPage && lastVisitTime) {
const timeSinceLastVisit = Date.now() - lastVisitTime;
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
// Restore the navigation history
if (savedHistory?.length) {
// First navigate to credentials page as the base
navigate('/credentials', { replace: true });
// Then restore the history stack
for (const entry of savedHistory) {
navigate(entry.pathname + entry.search + entry.hash);
}
return;
}
// Fallback to simple navigation if no history
navigate('/credentials', { replace: true });
navigate(lastPage, { replace: true });
return;
}
}
// Duration has expired, clear all stored navigation data
await Promise.all([
storage.removeItem(LAST_VISITED_PAGE_KEY),
storage.removeItem(LAST_VISITED_TIME_KEY),
storage.removeItem(NAVIGATION_HISTORY_KEY),
sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
]);
// Navigate to the credentials page as default entry page.
navigate('/credentials', { replace: true });
}, [navigate, isFullyInitialized, requiresAuth]);
// Handle initialization and auth state changes
useEffect(() => {
// Check for inline unlock mode
const urlParams = new URLSearchParams(window.location.search);
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
setIsInlineUnlockMode(inlineUnlock);
if (isFullyInitialized) {
setIsInitialLoading(false);
if (requiresAuth) {
const allowedPaths = ['/login', '/unlock', '/unlock-success', '/auth-settings'];
if (allowedPaths.includes(location.pathname)) {
// Do not override the navigation if the current path is in the allowed paths.
return;
}
// Determine which auth page to show
if (!isLoggedIn) {
navigate('/login', { replace: true });
} else if (!dbAvailable) {
navigate('/unlock', { replace: true });
} else if (inlineUnlock) {
navigate('/unlock-success', { replace: true });
}
} else if (!isInitialized) {
// First initialization, try to restore last page or go to credentials
restoreLastPage().then(() => {
setIsInitialized(true);
});
}
}
}, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, isInitialized, navigate, restoreLastPage, setIsInitialLoading, location.pathname]);
// Store the current page whenever it changes
useEffect(() => {
if (isInitialized) {
storeCurrentPage();
}
}, [location.pathname, location.search, location.hash, isInitialized, storeCurrentPage]);
const contextValue = useMemo(() => ({
storeCurrentPage,
restoreLastPage,
isFullyInitialized,
requiresAuth
}), [storeCurrentPage, restoreLastPage, isFullyInitialized, requiresAuth]);
return (
<NavigationContext.Provider value={contextValue}>
{children}
</NavigationContext.Provider>
);
};
/**
* Hook to access the navigation context.
* @returns The navigation context
*/
export const useNavigation = (): NavigationContextType => {
const context = useContext(NavigationContext);
if (context === undefined) {
throw new Error('useNavigation must be used within a NavigationProvider');
}
return context;
};

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import * as Yup from 'yup';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { AppInfo } from '@/utils/AppInfo';
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
@@ -55,6 +57,7 @@ const AuthSettings: React.FC = () => {
const [customClientUrl, setCustomClientUrl] = useState<string>('');
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({});
const { setIsInitialLoading } = useLoading();
useEffect(() => {
/**
@@ -83,10 +86,11 @@ const AuthSettings: React.FC = () => {
} else {
setSelectedOption(DEFAULT_OPTIONS[0].value);
}
setIsInitialLoading(false);
};
loadStoredSettings();
}, []);
}, [setIsInitialLoading]);
/**
* Handle option change

View File

@@ -4,11 +4,12 @@ import { yupResolver } from '@hookform/resolvers/yup';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import * as Yup from 'yup';
import { FormInput } from '@/entrypoints/popup/components/FormInput';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import Modal from '@/entrypoints/popup/components/Modal';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
@@ -24,6 +25,13 @@ import { useLoading } from '../context/LoadingContext';
type CredentialMode = 'random' | 'manual';
// Persisted form data type used for JSON serialization.
type PersistedFormData = {
credentialId: string | null;
mode: CredentialMode;
formValues: Omit<Credential, 'Logo'> & { Logo?: string | null };
}
/**
* Validation schema for the credential form.
*/
@@ -67,7 +75,7 @@ const CredentialAddEdit: React.FC = () => {
const [mode, setMode] = useState<CredentialMode>('random');
const { setHeaderButtons } = useHeaderButtons();
const { setIsInitialLoading } = useLoading();
const [localLoading, setLocalLoading] = useState(false);
const [localLoading, setLocalLoading] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const webApi = useWebApi();
@@ -94,19 +102,128 @@ const CredentialAddEdit: React.FC = () => {
}
});
/**
* Persists the current form values to storage
* @returns Promise that resolves when the form values are persisted
*/
const persistFormValues = useCallback(async (): Promise<void> => {
if (localLoading) {
// Do not persist values if the page is still loading.
return;
}
const formValues = watch();
const persistedData: PersistedFormData = {
credentialId: id || null,
mode,
formValues: {
...formValues,
Logo: null // Don't persist the Logo field as it can't be user modified in the UI.
}
};
await sendMessage('PERSIST_FORM_VALUES', JSON.stringify(persistedData), 'background');
}, [watch, id, mode, localLoading]);
/**
* Watch for mode changes and persist form values
*/
useEffect(() => {
if (!localLoading) {
void persistFormValues();
}
}, [mode, persistFormValues, localLoading]);
// Watch for form changes and persist them
useEffect(() => {
const subscription = watch(() => {
void persistFormValues();
});
return (): void => subscription.unsubscribe();
}, [watch, persistFormValues]);
// If we received an ID, we're in edit mode
const isEditMode = id !== undefined && id.length > 0;
/**
* Loads persisted form values from storage. This is used to keep track of form changes
* and restore them when the page is reloaded. The browser extension popup will close
* automatically by clicking outside of the popup, but with this logic we can restore
* the form values when the page is reloaded so the user can continue their mutation operation.
*
* @returns Promise that resolves when the form values are loaded
*/
const loadPersistedValues = useCallback(async (): Promise<void> => {
const persistedData = await sendMessage('GET_PERSISTED_FORM_VALUES', null, 'background') as string | null;
// Try to parse the persisted data as a JSON object.
try {
let persistedDataObject: PersistedFormData | null = null;
try {
if (persistedData) {
persistedDataObject = JSON.parse(persistedData) as PersistedFormData;
}
} catch (error) {
console.error('Error parsing persisted data:', error);
}
// Check if the object has a value and is not null
const objectEmpty = persistedDataObject === null || persistedDataObject === undefined;
if (objectEmpty) {
// If the persisted data object is empty, we don't have any values to restore and can exit early.
setLocalLoading(false);
return;
}
const isCurrentPage = persistedDataObject?.credentialId == id;
if (persistedDataObject && isCurrentPage) {
// Only restore if the persisted credential ID matches current page
setMode(persistedDataObject.mode);
Object.entries(persistedDataObject.formValues).forEach(([key, value]) => {
setValue(key as keyof Credential, value as Credential[keyof Credential]);
});
} else {
console.error('Persisted values do not match current page');
}
} catch (error) {
console.error('Error loading persisted data:', error);
}
// Set local loading state to false which also activates the persisting of form value changes from this point on.
setLocalLoading(false);
}, [setValue, id, setMode, setLocalLoading]);
/**
* Clears persisted form values from storage
* @returns Promise that resolves when the form values are cleared
*/
const clearPersistedValues = useCallback(async (): Promise<void> => {
await sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background');
}, []);
// Clear persisted values when the page is unmounted.
useEffect(() => {
return (): void => {
void clearPersistedValues();
};
}, [clearPersistedValues]);
/**
* Load an existing credential from the database in edit mode.
*/
useEffect(() => {
if (!dbContext?.sqliteClient || !id) {
if (!dbContext?.sqliteClient) {
return;
}
if (!id) {
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
setTimeout(() => {
serviceNameRef.current?.focus();
}, 100);
setIsInitialLoading(false);
// Load persisted form values if they exist.
loadPersistedValues();
return;
}
@@ -122,16 +239,19 @@ const CredentialAddEdit: React.FC = () => {
});
setMode('manual');
setIsInitialLoading(false);
// On create mode, focus the service name field after a short delay to ensure the component is mounted
// Check for persisted values that might override the loaded values if they exist.
loadPersistedValues();
} else {
console.error('Credential not found');
navigate('/credentials');
}
} catch (err) {
console.error('Error loading credential:', err);
setIsInitialLoading(false);
}
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue]);
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues]);
/**
* Handle the delete button click.
@@ -148,10 +268,11 @@ const CredentialAddEdit: React.FC = () => {
* Navigate to the credentials list page on success.
*/
onSuccess: () => {
void clearPersistedValues();
navigate('/credentials');
}
});
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate]);
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate, clearPersistedValues]);
/**
* Initialize the identity and password generators with settings from user's vault.
@@ -312,6 +433,7 @@ const CredentialAddEdit: React.FC = () => {
* Navigate to the credential details page on success.
*/
onSuccess: () => {
void clearPersistedValues();
// If in add mode, navigate to the credential details page.
if (!isEditMode) {
// Navigate to the credential details page.
@@ -322,7 +444,7 @@ const CredentialAddEdit: React.FC = () => {
}
},
});
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi]);
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues]);
// Set header buttons on mount and clear on unmount
useEffect((): (() => void) => {

View File

@@ -10,7 +10,7 @@ import {
NotesBlock
} from '@/entrypoints/popup/components/CredentialDetails';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
@@ -124,10 +124,10 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
email={credential.Alias.Email}
/>
)}
<NotesBlock notes={credential.Notes} />
<TotpBlock credentialId={credential.Id} />
<LoginCredentialsBlock credential={credential} />
<AliasBlock credential={credential} />
<NotesBlock notes={credential.Notes} />
</div>
);
};

View File

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import { useDb } from '@/entrypoints/popup/context/DbContext';

View File

@@ -15,7 +15,7 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import HeaderButton from '../components/HeaderButton';
import { HeaderIconType } from '../components/icons/HeaderIcons';
import { HeaderIconType } from '../components/Icons/HeaderIcons';
/**
* Email details page.

View File

@@ -1,62 +1,20 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import Login from '@/entrypoints/popup/pages/Login';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
import { useNavigation } from '@/entrypoints/popup/context/NavigationContext';
/**
* Home page that shows the correct page based on the user's authentication state.
* Most of the navigation logic is now handled by NavigationContext.
*/
const Home: React.FC = () => {
const authContext = useAuth();
const dbContext = useDb();
const navigate = useNavigate();
const { setIsInitialLoading } = useLoading();
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
const { isFullyInitialized } = useNavigation();
// Initialization state.
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
const isAuthenticated = authContext.isLoggedIn;
const isDatabaseAvailable = dbContext.dbAvailable;
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable || isInlineUnlockMode);
useEffect(() => {
// Detect if the user is coming from the unlock page with mode=inline_unlock.
const urlParams = new URLSearchParams(window.location.search);
const isInlineUnlockMode = urlParams.get('mode') === 'inline_unlock';
setIsInlineUnlockMode(isInlineUnlockMode);
// Redirect to credentials if fully initialized and doesn't need unlock.
if (isFullyInitialized && !requireLoginOrUnlock) {
navigate('/credentials', { replace: true });
}
}, [isFullyInitialized, requireLoginOrUnlock, isInlineUnlockMode, navigate]);
// Show loading state if not fully initialized or when about to redirect to credentials.
if (!isFullyInitialized || (isFullyInitialized && !requireLoginOrUnlock)) {
// Global loading spinner will be shown by the parent component.
if (!isFullyInitialized) {
return null;
}
setIsInitialLoading(false);
if (!isAuthenticated) {
return <Login />;
}
if (!isDatabaseAvailable) {
return <Unlock />;
}
if (isInlineUnlockMode) {
return <UnlockSuccess onClose={() => setIsInlineUnlockMode(false)} />;
}
return null;
return <Navigate to="/credentials" replace />;
};
export default Home;

View File

@@ -29,7 +29,7 @@ const Login: React.FC = () => {
username: '',
password: '',
});
const { showLoading, hideLoading } = useLoading();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
const [rememberMe, setRememberMe] = useState(true);
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
@@ -53,9 +53,10 @@ const Login: React.FC = () => {
}
setClientUrl(clientUrl);
setIsInitialLoading(false);
};
loadClientUrl();
}, []);
}, [setIsInitialLoading]);
/**
* Handle submit

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
import { sendMessage } from 'webext-bridge/popup';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';

View File

@@ -29,7 +29,7 @@ const Unlock: React.FC = () => {
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { showLoading, hideLoading } = useLoading();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
useEffect(() => {
/**
@@ -41,10 +41,11 @@ const Unlock: React.FC = () => {
if (statusError !== null) {
await webApi.logout(statusError);
}
setIsInitialLoading(false);
};
checkStatus();
}, [webApi, authContext]);
}, [webApi, authContext, setIsInitialLoading]);
/**
* Handle submit

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.19.0';
public static readonly VERSION = '0.19.2';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

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

View File

@@ -93,8 +93,8 @@ android {
applicationId 'net.aliasvault.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 5
versionName "0.19.0"
versionCode 6
versionName "0.19.2"
}
signingConfigs {
debug {

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "AliasVault",
"slug": "AliasVault",
"version": "0.19.0",
"version": "0.19.2",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "net.aliasvault.app",

View File

@@ -132,9 +132,9 @@ export default function CredentialDetailsScreen() : React.ReactNode {
</ThemedView>
<EmailPreview email={credential.Alias.Email} />
<TotpSection credential={credential} />
<NotesSection credential={credential} />
<LoginCredentials credential={credential} />
<AliasDetails credential={credential} />
<NotesSection credential={credential} />
</ThemedScrollView>
</ThemedContainer>
);

View File

@@ -30,6 +30,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
const [isSpamOk, setIsSpamOk] = useState(false);
const [isComponentVisible, setIsComponentVisible] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
const webApi = useWebApi();
const dbContext = useDb();
const authContext = useAuth();
@@ -48,6 +49,19 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
return metadata.publicEmailDomains.includes(emailAddress.split('@')[1]);
}, [dbContext]);
/**
* Check if the email is a private domain.
*/
const isPrivateDomain = useCallback(async (emailAddress: string): Promise<boolean> => {
// Get private domains from stored metadata
const metadata = await dbContext?.sqliteClient?.getVaultMetadata();
if (!metadata) {
return false;
}
return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]);
}, [dbContext]);
// Handle app state changes
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState): void => {
@@ -86,7 +100,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
}
const isPublic = await isPublicDomain(email);
const isPrivate = await isPrivateDomain(email);
const isSupported = isPublic || isPrivate;
setIsSpamOk(isPublic);
setIsSupportedDomain(isSupported);
if (!isSupported) {
return;
}
if (isPublic) {
// For public domains (SpamOK), use the SpamOK API directly
@@ -116,7 +138,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
}
setEmails(latestMails);
} else {
} else if (isPrivate) {
// For private domains, use existing encrypted email logic
if (!dbContext?.sqliteClient) {
return;
@@ -186,7 +208,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
clearInterval(interval);
}
};
}, [email, loading, webApi, dbContext, isPublicDomain, authContext.isOffline, isComponentVisible]);
}, [email, loading, webApi, dbContext, isPublicDomain, isPrivateDomain, authContext.isOffline, isComponentVisible]);
const styles = StyleSheet.create({
date: {
@@ -244,6 +266,11 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
return null;
}
// Don't render anything if the domain is not supported
if (!isSupportedDomain) {
return null;
}
if (error) {
return (
<ThemedView style={styles.section}>

View File

@@ -1041,7 +1041,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -1056,7 +1056,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1081,7 +1081,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
INFOPLIST_FILE = AliasVault/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
@@ -1091,7 +1091,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1235,7 +1235,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1288,7 +1288,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1337,7 +1337,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1372,7 +1372,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1405,7 +1405,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1458,7 +1458,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1507,7 +1507,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1559,7 +1559,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1610,7 +1610,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1626,7 +1626,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@@ -1655,7 +1655,7 @@
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1671,7 +1671,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.19.0;
MARKETING_VERSION = 0.19.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;

View File

@@ -5,10 +5,17 @@ import VaultModels
public struct CredentialCard: View {
let credential: Credential
let action: () -> Void
let onCopy: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var showCopyToast = false
@State private var copyToastMessage = ""
public init(credential: Credential, action: @escaping () -> Void, onCopy: @escaping () -> Void) {
self.credential = credential
self.action = action
self.onCopy = onCopy
}
public var body: some View {
Button(action: action) {
HStack(spacing: 16) {
@@ -42,39 +49,56 @@ public struct CredentialCard: View {
.cornerRadius(8)
}
.contextMenu(menuItems: {
Button(action: {
if let username = credential.username {
if let username = credential.username, !username.isEmpty {
Button(action: {
UIPasteboard.general.string = username
copyToastMessage = "Username copied"
showCopyToast = true
}
}, label: {
Label("Copy Username", systemImage: "person")
})
Button(action: {
if let password = credential.password?.value {
// Delay for 1 second before calling onCopy which dismisses the view
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
onCopy()
}
}, label: {
Label("Copy Username", systemImage: "person")
})
}
if let password = credential.password?.value, !password.isEmpty {
Button(action: {
UIPasteboard.general.string = password
copyToastMessage = "Password copied"
showCopyToast = true
}
}, label: {
Label("Copy Password", systemImage: "key")
})
// Delay for 1 second before calling onCopy which dismisses the view
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
onCopy()
}
}, label: {
Label("Copy Password", systemImage: "key")
})
}
Button(action: {
if let email = credential.alias?.email {
if let email = credential.alias?.email, !email.isEmpty {
Button(action: {
UIPasteboard.general.string = email
copyToastMessage = "Email copied"
showCopyToast = true
}
}, label: {
Label("Copy Email", systemImage: "envelope")
})
// Delay for 1 second before calling onCopy which dismisses the view
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
onCopy()
}
}, label: {
Label("Copy Email", systemImage: "envelope")
})
}
Divider()
if (credential.username != nil && !credential.username!.isEmpty) ||
(credential.password?.value != nil && !credential.password!.value.isEmpty) ||
(credential.alias?.email != nil && !credential.alias!.email!.isEmpty) {
Divider()
}
Button(action: {
if let url = URL(string: "aliasvault://credentials/\(credential.id.uuidString)") {
if let url = URL(string: "net.aliasvault.app://credentials/\(credential.id.uuidString)") {
UIApplication.shared.open(url)
}
}, label: {
@@ -82,7 +106,7 @@ public struct CredentialCard: View {
})
Button(action: {
if let url = URL(string: "aliasvault://credentials/add-edit-page?id=\(credential.id.uuidString)") {
if let url = URL(string: "net.aliasvault.app://credentials/add-edit-page?id=\(credential.id.uuidString)") {
UIApplication.shared.open(url)
}
}, label: {
@@ -97,7 +121,7 @@ public struct CredentialCard: View {
Text(copyToastMessage)
.padding()
.background(Color.black.opacity(0.7))
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.accentBackground : ColorConstants.Light.accentBackground)
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.text : ColorConstants.Light.text)
.cornerRadius(8)
.padding(.bottom, 20)
}
@@ -176,6 +200,7 @@ public func truncateText(_ text: String?, limit: Int) -> String {
updatedAt: Date(),
isDeleted: false
),
action: {}
action: {},
onCopy: {}
)
}

View File

@@ -81,9 +81,11 @@ public struct CredentialProviderView: View {
} else {
LazyVStack(spacing: 8) {
ForEach(viewModel.filteredCredentials, id: \.service) { credential in
CredentialCard(credential: credential) {
CredentialCard(credential: credential, action: {
viewModel.selectCredential(credential)
}
}, onCopy: {
viewModel.cancel()
})
}
}
.padding(.horizontal)

View File

@@ -8,7 +8,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.19.0';
public static readonly VERSION = '0.19.2';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<base href="//@NavigationService.BaseUri.Replace("http://", "").Replace("https://", "")"/>
<base href="@(HttpContext.Request.Headers["X-Forwarded-Prefix"].FirstOrDefault() ?? "/")"/>
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/tailwind.css")"/>
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/app.css")"/>
<link rel="stylesheet" href="AliasVault.Admin.styles.css"/>

View File

@@ -104,6 +104,16 @@ builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
var app = builder.Build();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost,
RequireHeaderSymmetry = false,
ForwardLimit = null,
ForwardedProtoHeaderName = "X-Forwarded-Proto",
ForwardedHostHeaderName = "X-Forwarded-Host",
ForwardedForHeaderName = "X-Forwarded-For",
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@@ -123,13 +133,6 @@ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBAS
app.UsePathBase(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBASE"));
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost,
RequireHeaderSymmetry = false,
ForwardLimit = null,
});
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();

View File

@@ -1,6 +1,6 @@
<div class="relative p-6 sm:p-8 bg-white dark:bg-gray-700 rounded-lg sm:shadow-xl max-w-md w-full mx-auto">
<div class="text-center">
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault decryption error.</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">An error occured while locally decrypting your vault. Your data is not accessible at this moment. Please try again (later) or contact support.</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">An error occurred while locally decrypting your vault. Your data is not accessible at this moment. Please try again (later) or contact support.</p>
</div>
</div>

View File

@@ -30,7 +30,7 @@ public static class AppInfo
/// <summary>
/// Gets the patch version number.
/// </summary>
public const int VersionPatch = 0;
public const int VersionPatch = 2;
/// <summary>
/// Gets the minimum supported AliasVault client version. Normally the minimum client version is the same

View File

@@ -76,10 +76,14 @@ http {
# Admin interface
location /admin {
proxy_pass http://admin;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Prefix /admin/;
# Rewrite HTTP redirects to HTTPS
proxy_redirect http:// https://;
# Add WebSocket support for Blazor server
proxy_http_version 1.1;
@@ -91,7 +95,7 @@ http {
# API endpoints
location /api {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -100,10 +104,13 @@ http {
# Client app (root path)
location / {
proxy_pass http://client;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Rewrite HTTP redirects to HTTPS
proxy_redirect http:// https://;
}
}
}