Compare commits

..

13 Commits

Author SHA1 Message Date
Leendert de Borst
5baede08a7 Bump version to 0.12.2 (#616) 2025-02-25 13:41:55 +01:00
Leendert de Borst
34995fe801 Fix cueck if client or api url are empty (#612) 2025-02-25 12:48:53 +01:00
Leendert de Borst
92a2511d9d Fix bug in browser extension emails list if credential has no email address (#612) 2025-02-25 12:48:53 +01:00
Leendert de Borst
41486c940c Update max nginx upload filesize to 25MB (#613) 2025-02-25 12:48:37 +01:00
Leendert de Borst
47c77ade02 Update install.sh to set default ip_logging value (#610) 2025-02-25 12:48:13 +01:00
Leendert de Borst
a51621970d Update browser-extension-tests.yml 2025-02-24 21:46:42 +01:00
Leendert de Borst
39f339b659 Bump version to 0.12.1 (#608) 2025-02-24 21:28:08 +01:00
Leendert de Borst
65d1ca1564 Add try catch for incorrect status login call (#601) 2025-02-24 21:27:57 +01:00
Leendert de Borst
5c010cd873 Add private/public email validation before showing recent emails (#602) 2025-02-24 21:20:21 +01:00
Leendert de Borst
88ba57ce88 Fix chrome extension API URL switching (#600) 2025-02-24 21:20:12 +01:00
Leendert de Borst
4d266beb0d Add anchor tag conversion to open in new tab in email display (#603) 2025-02-24 21:20:02 +01:00
Leendert de Borst
536688d110 Enable manual workflow dispatch for release archive logic 2025-02-24 18:16:10 +01:00
Leendert de Borst
e343b48fe7 Update browser-extension-tests.yml 2025-02-24 18:10:54 +01:00
18 changed files with 171 additions and 37 deletions

View File

@@ -7,6 +7,7 @@ on:
branches: [ "main" ]
release:
types: [published]
workflow_dispatch:
jobs:
chrome-extension:
@@ -60,11 +61,10 @@ jobs:
- name: Zip Chrome Extension
run: |
cd browser-extensions/chrome/dist
zip -r ../../aliasvault-chrome-extension.zip .
zip -r ../../../aliasvault-chrome-extension.zip .
- name: Upload Chrome Extension ZIP to Release
uses: softprops/action-gh-release@v2
with:
files: aliasvault-chrome-extension.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -58,7 +58,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
```bash
# Download install script from latest stable release
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.12.0/install.sh
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.12.2/install.sh
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
chmod +x install.sh

View File

@@ -1,7 +1,7 @@
{
"name": "AliasVault",
"description": "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
"version": "0.12.0",
"version": "0.12.2",
"manifest_version": 3,
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"

View File

@@ -58,7 +58,8 @@ const App: React.FC = () => {
useEffect(() => {
if (authContext.globalMessage) {
setMessage(authContext.globalMessage);
authContext.clearGlobalMessage();
} else {
setMessage(null);
}
}, [authContext, authContext.globalMessage]);

View File

@@ -30,7 +30,7 @@ const Header: React.FC<HeaderProps> = ({
const openClientTab = async () : Promise<void> => {
const setting = await chrome.storage.local.get(['clientUrl']);
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
if (setting.clientUrl.length > 0) {
if (setting.clientUrl && setting.clientUrl.length > 0) {
clientUrl = setting.clientUrl;
}

View File

@@ -9,6 +9,8 @@ type DbContextType = {
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<void>;
clearDatabase: () => void;
vaultRevision: number;
publicEmailDomains: string[];
privateEmailDomains: string[];
}
const DbContext = createContext<DbContextType | undefined>(undefined);
@@ -35,7 +37,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
/**
* Public email domains.
*/
const [, setPublicEmailDomains] = useState<string[]>([]);
const [publicEmailDomains, setPublicEmailDomains] = useState<string[]>([]);
/**
* Vault revision.
@@ -45,7 +47,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
/**
* Private email domains.
*/
const [, setPrivateEmailDomains] = useState<string[]>([]);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
// Attempt to decrypt the blob.
@@ -123,8 +125,10 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
dbAvailable,
initializeDatabase,
clearDatabase,
vaultRevision
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, vaultRevision]);
vaultRevision,
publicEmailDomains,
privateEmailDomains
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, vaultRevision, publicEmailDomains, privateEmailDomains]);
return (
<DbContext.Provider value={contextValue}>

View File

@@ -44,6 +44,30 @@ const CredentialDetails: React.FC = () => {
window.close();
};
/**
* Checks if the email domain is supported for email preview.
*
* @param email The email address to check
* @returns True if the domain is supported, false otherwise
*/
const isEmailDomainSupported = (email: string): boolean => {
// Extract domain from email
const domain = email.split('@')[1]?.toLowerCase();
if (!domain) {
return false;
}
// Check if domain is in public or private domains
const publicDomains = dbContext.publicEmailDomains ?? [];
const privateDomains = dbContext.privateEmailDomains ?? [];
// Check if the domain ends with any of the supported domains
return [...publicDomains, ...privateDomains].some(supportedDomain =>
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
);
};
useEffect(() => {
// For popup windows, ensure we have proper history state for navigation
if (isPopup()) {
@@ -121,11 +145,15 @@ const CredentialDetails: React.FC = () => {
</div>
{credential.Email && (
<div className="mt-6">
<EmailPreview
email={credential.Email}
/>
</div>
<>
{isEmailDomainSupported(credential.Email) && (
<div className="mt-6">
<EmailPreview
email={credential.Email}
/>
</div>
)}
</>
)}
</div>

View File

@@ -8,6 +8,7 @@ import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
import EncryptionUtility from '../../shared/EncryptionUtility';
import { Attachment } from '../../shared/types/webapi/Attachment';
import { useLoading } from '../context/LoadingContext';
import ConversionUtility from '../utils/ConversionUtility';
/**
* Email details page.
@@ -227,7 +228,7 @@ const EmailDetails: React.FC = () => {
<div className="bg-white">
{email.messageHtml ? (
<iframe
srcDoc={email.messageHtml}
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
className="w-full min-h-[500px] border-0"
title="Email content"
/>

View File

@@ -34,13 +34,8 @@ const EmailsList: React.FC = () => {
return;
}
// TODO: create separate query to only get email addresses to avoid loading all credentials.
const credentials = dbContext.sqliteClient.getAllCredentials();
// Get unique email addresses from all credentials.
const emailAddresses = credentials
.map(cred => cred.Email.trim()) // Trim whitespace
.filter((email, index, self) => self.indexOf(email) === index);
const emailAddresses = dbContext.sqliteClient.getAllEmailAddresses();
try {
// For now we only show the latest 50 emails. No pagination.

View File

@@ -10,6 +10,7 @@ import { useLoading } from '../context/LoadingContext';
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import { LoginResponse } from '../../shared/types/webapi/Login';
import LoginServerInfo from '../components/LoginServerInfo';
import { AppInfo } from '../../shared/AppInfo';
/**
* Login page
@@ -39,7 +40,12 @@ const Login: React.FC = () => {
*/
const loadClientUrl = async () : Promise<void> => {
const setting = await chrome.storage.local.get(['clientUrl']);
setClientUrl(setting.clientUrl);
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
if (setting.clientUrl && setting.clientUrl.length > 0) {
clientUrl = setting.clientUrl;
}
setClientUrl(clientUrl);
};
loadClientUrl();
}, []);
@@ -54,6 +60,9 @@ const Login: React.FC = () => {
try {
showLoading();
// Clear global message if set with every login attempt.
authContext.clearGlobalMessage();
// Use the srpUtil instance instead of the imported singleton
const loginResponse = await srpUtil.initiateLogin(credentials.username);
@@ -121,9 +130,8 @@ const Login: React.FC = () => {
// Show app.
hideLoading();
} catch (err) {
setError('Login failed. Please check your credentials and try again.');
console.error('Login error:', err);
} catch {
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
hideLoading();
}
};

View File

@@ -0,0 +1,56 @@
/**
* Utility class for conversion operations.
*/
class ConversionUtility {
/**
* Convert all anchor tags to open in a new tab.
* @param html HTML input.
* @returns HTML with all anchor tags converted to open in a new tab when clicked on.
*
* Note: same implementation exists in c-sharp version in AliasVault.Shared.Utilities.ConversionUtility.cs
*/
public convertAnchorTagsToOpenInNewTab(html: string): string {
try {
// Create a DOM parser
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Select all anchor tags with href attribute
const anchors = doc.querySelectorAll('a[href]');
if (anchors.length > 0) {
anchors.forEach((anchor: Element) => {
// Handle target attribute
if (!anchor.hasAttribute('target')) {
anchor.setAttribute('target', '_blank');
} else if (anchor.getAttribute('target') !== '_blank') {
anchor.setAttribute('target', '_blank');
}
// Handle rel attribute for security
if (!anchor.hasAttribute('rel')) {
anchor.setAttribute('rel', 'noopener noreferrer');
} else {
const relValue = anchor.getAttribute('rel') ?? '';
const relValues = new Set(relValue.split(' ').filter(val => val.trim() !== ''));
relValues.add('noopener');
relValues.add('noreferrer');
anchor.setAttribute('rel', Array.from(relValues).join(' '));
}
});
}
return doc.documentElement.outerHTML;
} catch (ex) {
// Log the exception
console.error(`Error in convertAnchorTagsToOpenInNewTab: ${ex instanceof Error ? ex.message : String(ex)}`);
// Return the original HTML if an error occurs
return html;
}
}
}
export default new ConversionUtility();

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

View File

@@ -229,6 +229,25 @@ class SqliteClient {
}));
}
/**
* Fetch all unique email addresses from all credentials.
* @returns Array of email addresses.
*/
public getAllEmailAddresses(): string[] {
const query = `
SELECT DISTINCT
a.Email
FROM Credentials c
LEFT JOIN Aliases a ON c.AliasId = a.Id
WHERE a.Email IS NOT NULL AND a.Email != '' AND c.IsDeleted = 0
`;
const results = this.executeQuery(query);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return results.map((row: any) => row.Email);
}
/**
* Fetch all encryption keys.
*/

View File

@@ -21,18 +21,18 @@ export class WebApiService {
*
* @param {Function} handleLogout - Function to handle logout.
*/
public constructor(
private readonly handleLogout: () => void
) {
// Remove initialization of baseUrl
}
public constructor(private readonly handleLogout: () => void) { }
/**
* Get the base URL for the API from settings.
*/
private async getBaseUrl(): Promise<string> {
const result = await chrome.storage.local.get(['apiUrl']);
return (result.apiUrl ?? AppInfo.DEFAULT_API_URL).replace(/\/$/, '') + '/v1/';
if (result.apiUrl && result.apiUrl.length > 0) {
return result.apiUrl.replace(/\/$/, '') + '/v1/';
}
return AppInfo.DEFAULT_API_URL.replace(/\/$/, '') + '/v1/';
}
/**
@@ -215,13 +215,29 @@ export class WebApiService {
* Calls the status endpoint to check if the auth tokens are still valid, app is supported and the vault is up to date.
*/
public async getStatus(): Promise<StatusResponse> {
return await this.get<StatusResponse>('Auth/status');
try {
return await this.get<StatusResponse>('Auth/status');
} catch {
/**
* If the status endpoint is not available, return a default status response which will trigger
* a logout and error message.
*/
return {
clientVersionSupported: true,
serverVersion: '0.0.0',
vaultRevision: 0
};
}
}
/**
* Validates the status response and returns an error message if validation fails.
*/
public validateStatusResponse(statusResponse: StatusResponse): string | null {
if (statusResponse.serverVersion === '0.0.0') {
return 'The AliasVault server is not available. Please try again later or contact support if the problem persists.';
}
if (!statusResponse.clientVersionSupported) {
return 'This version of the AliasVault browser extension is outdated. Please update your browser extension to the latest version.';
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# @version 0.12.0
# @version 0.12.2
# Repository information used for downloading files and images from GitHub
REPO_OWNER="lanedirt"
@@ -1566,6 +1566,7 @@ handle_install_version() {
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
set_default_ports || { printf "${RED}> Failed to set default ports${NC}\n"; exit 1; }
set_public_registration || { printf "${RED}> Failed to set public registration${NC}\n"; exit 1; }
set_ip_logging || { printf "${RED}> Failed to set IP logging${NC}\n"; exit 1; }
# Only generate admin password if not already set
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then

View File

@@ -3,6 +3,8 @@ events {
}
http {
client_max_body_size 25M;
upstream client {
server client:3000;
}

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 a dictionary of minimum supported client versions that the WebApi supports.

View File

@@ -19,6 +19,9 @@ public static class ConversionUtility
/// </summary>
/// <param name="html">HTML input.</param>
/// <returns>HTML with all anchor tags converted to open in a new tab when clicked on.</returns>
/// <remarks>
/// Note: same implementation exists in browser extension Typescript version in ConversionUtility.ts.
/// </remarks>
public static string ConvertAnchorTagsToOpenInNewTab(string html)
{
try