Compare commits

...

36 Commits

Author SHA1 Message Date
Leendert de Borst
206254574a Bump version to 0.12.3 (#626) 2025-02-28 19:01:36 +01:00
Leendert de Borst
9a9fb12d73 Merge pull request #625 from lanedirt/622-make-browser-extension-autofill-popup-more-robust
Make browser extension autofill popup more robust
2025-02-28 18:39:04 +01:00
Leendert de Borst
5d0540ee2b Autofill form directly after creating new credential (#622) 2025-02-28 18:15:37 +01:00
Leendert de Borst
59726d87e8 Add full name form detection test (#622) 2025-02-28 13:30:35 +01:00
Leendert de Borst
7dccb6443a Improve firstname/lastname detection (#624) 2025-02-28 12:41:33 +01:00
Leendert de Borst
451fe98102 Add full name to form detector (#622) 2025-02-28 12:40:17 +01:00
Leendert de Borst
a82b7d7ce5 Update tailwind css (#622) 2025-02-28 12:12:23 +01:00
Leendert de Borst
9cbaf51778 Merge branch 'main' into 622-make-browser-extension-autofill-popup-more-robust 2025-02-28 12:00:04 +01:00
Leendert de Borst
1847293162 Merge pull request #621 from lanedirt/620-add-browser-extension-download-links-to-client
Add browser extension page and download links
2025-02-27 22:22:38 +01:00
Leendert de Borst
e5a174443d Merge branch 'main' into 620-add-browser-extension-download-links-to-client 2025-02-27 22:22:01 +01:00
Leendert de Borst
2382ee6592 Update brave detection and responsive design (#620) 2025-02-27 20:23:13 +01:00
Leendert de Borst
7253d1fee2 Do all logout actions via webapi which calls authcontext too (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
bc16167293 Refactor (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
eb587e3496 Add webapi logout call to all places (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
6d0352923a Simplify main logout flow to use page redirect (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
6d33f99d62 Update favicon display in client to handle SVG (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
9fbdb2efbb Update form detection and popup icon display (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
50817b65d3 Make popup create button more robust (#622) 2025-02-27 17:53:48 +01:00
Leendert de Borst
5750eef248 Add client header to webapi token refresh call (#618) 2025-02-27 17:52:01 +01:00
Leendert de Borst
5cd5efca4a Do all logout actions via webapi which calls authcontext too (#622) 2025-02-27 17:35:28 +01:00
Leendert de Borst
7ce841b4b5 Refactor (#622) 2025-02-27 17:19:33 +01:00
Leendert de Borst
5e1c79610f Add webapi logout call to all places (#622) 2025-02-27 16:54:54 +01:00
Leendert de Borst
a2ccee984b Simplify main logout flow to use page redirect (#622) 2025-02-27 16:39:33 +01:00
Leendert de Borst
f9977fb29e Update favicon display in client to handle SVG (#622) 2025-02-27 16:11:23 +01:00
Leendert de Borst
f8ea8fc7ce Update form detection and popup icon display (#622) 2025-02-27 15:48:56 +01:00
Leendert de Borst
4ab5be17c0 Make popup create button more robust (#622) 2025-02-27 15:24:54 +01:00
Leendert de Borst
ad8f13928e Make browser extension highlight/other dynamic (#620) 2025-02-27 14:12:00 +01:00
Leendert de Borst
29af7c2196 Add browser extension page and download links (#620) 2025-02-27 12:24:51 +01:00
Leendert de Borst
b25f6580cd Update README.md 2025-02-26 17:17:20 +01:00
Leendert de Borst
71ae5d0904 Update browser-extension-tests.yml 2025-02-25 13:50:34 +01:00
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
41 changed files with 620 additions and 126 deletions

View File

@@ -61,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-${{ github.ref_name }}.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 }}
files: aliasvault-chrome-extension-${{ github.ref_name }}.zip
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -27,7 +27,7 @@
</div>
AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. The core of AliasVault is built with C# ASP.NET Blazor WASM technology. AliasVault can be self-hosted on your own server with Docker.
AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. AliasVault can be self-hosted on your own server with Docker.
### What makes AliasVault unique:
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
@@ -42,9 +42,8 @@ The official cloud version of AliasVault is freely available at [app.aliasvault.
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
## Self-host
To self-host and install AliasVault on your own server, the easiest method is to use the provided install script. This will download the pre-built Docker images and start the containers.
## Self-hosting
For full control over your own data you can self-host and install AliasVault on your own servers. The easiest method is to use the provided install script. This will download the pre-built Docker images and start the containers.
### Install using install script
@@ -58,7 +57,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.1/install.sh
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.12.3/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
@@ -75,8 +74,6 @@ The install script will output the URL where the app is available. By default th
For more detailed information about the installation process and other topics, please see the official documentation website:
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
Here you can also find step-by-step instructions on how to install AliasVault to e.g. Azure, AWS and other popular cloud providers.
## Security Architecture
<a href="https://docs.aliasvault.net/architecture"><img alt="AliasVault Security Architecture Diagram" src="docs/assets/diagrams/security-architecture/aliasvault-security-architecture-thumb.jpg" width="343"></a>
@@ -97,7 +94,7 @@ AliasVault is under active development with new features being added regularly.
- [x] End-to-end encryption
- [x] Built-in email server for aliases
- [x] Single-command Docker-based installation
- [x] Chrome browser extension - (Pending Chrome Web Store approval. Manual installation possible, see latest release)
- [x] Chrome browser extension
- [ ] Firefox browser extension (https://github.com/lanedirt/AliasVault/issues/581)
- [ ] Add and associate TOTP MFA tokens to credentials (https://github.com/lanedirt/AliasVault/issues/181)
- [ ] Add support for connecting custom user domains to cloud hosted version (https://github.com/lanedirt/AliasVault/issues/485)
@@ -110,22 +107,25 @@ AliasVault is under active development with new features being added regularly.
Want to suggest a feature? Join our [Discord](https://discord.gg/DsaXMTEtpF) or create an issue on GitHub.
## Tech stack / credits
The following technologies, frameworks and libraries are used in this project:
## Tech Stack & Security
- [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) - A simple, modern, object-oriented, and type-safe programming language.
- [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) - An open-source framework for building modern multi-platform web applications.
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) - Object-relational mapping framework for .NET.
- [Blazor WASM](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript. It's a single-page app framework that runs in the browser via WebAssembly.
- [PostgreSQL](https://www.postgresql.org/) - An open-source object-relational database system used as the database for the server.
- [Docker](https://www.docker.com/) - Used for containerizing the server and client apps.
- [SQLite](https://www.sqlite.org/index.html) - A C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine. Used as database engine for the encrypted user's vault.
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library based on Tailwind CSS.
- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm.
- [SRP.net](https://github.com/secure-remote-password/srp.net) - SRP6a Secure Remote Password protocol for secure password authentication without sending plaintext passwords over the network.
- [Playwright](https://playwright.dev/) - A Node.js library to automate Chromium, Firefox and WebKit with a single API. Used for end-to-end testing.
- [SmtpServer](https://github.com/cosullivan/SmtpServer) - A SMTP server library for .NET that is used for the virtual email address feature.
- [MimeKit](https://github.com/jstedfast/MimeKit) - A .NET MIME creation and parser library used for the virtual email address feature.
- [StyleCop.Analyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) - Static code analysis tool that enforces style and consistency rules for C# code.
- [SonarQube Cloud](https://www.sonarqube.org/) - A platform for continuous code quality management.
AliasVault is built with a modern, secure, and scalable technology stack, ensuring robust encryption and privacy protection.
### Core Technologies
- **C# & ASP.NET Core** Reliable, high-performance backend for Web API.
- **Blazor WASM** Secure, interactive web UI.
- **PostgreSQL & SQLite** Database solutions, with SQLite powering encrypted user vaults.
- **Docker** Containerized deployment for scalability.
- **Next.JS & React & Typescript** - Powering the AliasVault website and browser extensions
### Security & Cryptography
- **Argon2id (Konscious.Security.Cryptography)** Industry-leading password hashing.
- **SRP** Secure Remote Password (SRP-6a) protocol for authentication.
- **MimeKit & SmtpServer** Secure email processing and virtual addresses.
### Additional Tools
- **Tailwind CSS & Flowbite** Modern UI design.
- **Playwright** Automated end-to-end testing.
- **SonarCloud** Continuous code quality monitoring.
AliasVault prioritizes security, performance, and user privacy with a technology stack trusted by the industry.

View File

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

View File

@@ -9,12 +9,13 @@ import CredentialsList from './pages/CredentialsList';
import EmailsList from './pages/EmailsList';
import LoadingSpinner from './components/LoadingSpinner';
import Home from './pages/Home';
import './style.css';
import CredentialDetails from './pages/CredentialDetails';
import EmailDetails from './pages/EmailDetails';
import Settings from './pages/Settings';
import GlobalStateChangeHandler from './components/GlobalStateChangeHandler';
import { useLoading } from './context/LoadingContext';
import Logout from './pages/Logout';
import './style.css';
/**
* Route configuration.
@@ -44,6 +45,7 @@ const App: React.FC = () => {
{ path: '/emails', element: <EmailsList />, showBackButton: false },
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
{ path: '/settings', element: <Settings />, showBackButton: false },
{ path: '/logout', element: <Logout />, showBackButton: false },
];
useEffect(() => {

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

@@ -47,10 +47,7 @@ export const UserMenu: React.FC = () => {
*/
const onLogout = async () : Promise<void> => {
showLoading();
await authContext.logout();
navigate('/', { replace: true });
// Delay for 100ms for improved UX
await new Promise(resolve => setTimeout(resolve, 100));
navigate('/logout', { replace: true });
hideLoading();
};

View File

@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
import SqliteClient from '../../shared/SqliteClient';
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import EncryptionUtility from '../../shared/EncryptionUtility';
type DbContextType = {
sqliteClient: SqliteClient | null;
dbInitialized: boolean;

View File

@@ -16,7 +16,13 @@ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ childr
*/
useEffect(() : void => {
const service = new WebApiService(
logout
(statusError: string | null) => {
if (statusError) {
logout(statusError);
} else {
logout();
}
}
);
setWebApiService(service);
}, [logout]);

View File

@@ -7,7 +7,6 @@ import { useLoading } from '../context/LoadingContext';
import { useWebApi } from '../context/WebApiContext';
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import ReloadButton from '../components/ReloadButton';
import { useAuth } from '../context/AuthContext';
import LoadingSpinner from '../components/LoadingSpinner';
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
@@ -21,7 +20,6 @@ const CredentialsList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const navigate = useNavigate();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
const authContext = useAuth();
/**
* Loading state with minimum duration for more fluid UX.
@@ -40,7 +38,7 @@ const CredentialsList: React.FC = () => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
authContext.logout(statusError);
await webApi.logout(statusError);
return;
}
@@ -60,7 +58,7 @@ const CredentialsList: React.FC = () => {
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
if (vaultError) {
authContext.logout(vaultError);
await webApi.logout(vaultError);
hideLoading();
return;
}
@@ -73,7 +71,7 @@ const CredentialsList: React.FC = () => {
} catch (err) {
console.error('Refresh error:', err);
}
}, [dbContext, webApi, authContext, hideLoading]);
}, [dbContext, webApi, hideLoading]);
/**
* Manually refresh the credentials list.

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

@@ -41,7 +41,7 @@ const Login: React.FC = () => {
const loadClientUrl = 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

@@ -0,0 +1,32 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useWebApi } from '../context/WebApiContext';
/**
* Logout page.
*/
const Logout: React.FC = () => {
const authContext = useAuth();
const webApi = useWebApi();
const navigate = useNavigate();
/**
* Logout and navigate to home page.
*/
useEffect(() => {
/**
* Perform logout via async method to ensure logout is completed before navigating to home page.
*/
const performLogout = async () : Promise<void> => {
await webApi.logout();
navigate('/');
};
performLogout();
}, [authContext, navigate, webApi]);
// Return null since this is just a functional component that handles logout.
return null;
};
export default Logout;

View File

@@ -31,7 +31,7 @@ const Unlock: React.FC = () => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
authContext.logout(statusError);
await webApi.logout(statusError);
}
};
@@ -81,26 +81,6 @@ const Unlock: React.FC = () => {
}
};
/**
* Handle logout
*/
const handleLogout = async () : Promise<void> => {
showLoading();
try {
await webApi.logout();
} catch (err) {
console.error('Logout error:', err);
}
try {
await authContext.logout();
} catch (err) {
console.error('Logout error:', err);
} finally {
hideLoading();
}
};
return (
<div className="max-w-md">
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
@@ -136,7 +116,7 @@ const Unlock: React.FC = () => {
</Button>
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
Switch accounts? <a href="#" onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
Switch accounts? <a href="/logout" className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
</div>
</form>
</div>

View File

@@ -66,6 +66,10 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
form.emailConfirmField.value = credential.Email;
triggerInputEvents(form.emailConfirmField);
}
if (form.fullNameField) {
form.fullNameField.value = `${credential.Alias.FirstName} ${credential.Alias.LastName}`;
triggerInputEvents(form.fullNameField);
}
if (form.firstNameField) {
form.firstNameField.value = credential.Alias.FirstName;
triggerInputEvents(form.firstNameField);
@@ -273,7 +277,7 @@ export function injectIcon(input: HTMLInputElement): void {
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2147483647;
z-index: 2147483640;
`;
document.body.appendChild(overlayContainer);
}
@@ -354,7 +358,7 @@ function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : voi
const rect = element.getBoundingClientRect();
overlay.style.cssText = `
position: fixed;
z-index: 999999999;
z-index: 999999991;
pointer-events: none;
top: ${rect.top}px;
left: ${rect.left}px;

View File

@@ -34,7 +34,7 @@ export function createBasePopup(input: HTMLInputElement) : HTMLElement {
popup.style.cssText = `
all: unset;
position: absolute;
z-index: 999999999;
z-index: 999999991;
background: ${isDarkMode() ? '#1f2937' : 'white'};
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
border-radius: 4px;
@@ -228,7 +228,14 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
createButton.style.backgroundColor = isDarkMode() ? '#374151' : '#f3f4f6';
});
createButton.addEventListener('click', async () => {
/**
* Handle create button click
*/
const handleCreateClick = async (e: Event) : Promise<void> => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// Determine service name based on conditions
let suggestedName = document.title;
@@ -331,8 +338,11 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
};
chrome.runtime.sendMessage({ type: 'CREATE_IDENTITY', credential }, () => {
// Refresh the popup to show new identity
openAutofillPopup(input);
// Close popup.
removeExistingPopup();
// Fill the form with the new identity immediately.
fillCredential(credential, input);
});
} catch (error) {
console.error('Error creating identity:', error);
@@ -345,9 +355,32 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
removeExistingPopup();
}, 2000);
}
};
// Add click listener with capture and prevent removal.
createButton.addEventListener('click', handleCreateClick, {
capture: true,
passive: false
});
// Create search input instead of button
// Backup click handling using mousedown/mouseup if needed.
let isMouseDown = false;
createButton.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
isMouseDown = true;
}, { capture: true });
createButton.addEventListener('mouseup', (e) => {
e.preventDefault();
e.stopPropagation();
if (isMouseDown) {
handleCreateClick(e);
}
isMouseDown = false;
}, { capture: true });
// Create search input.
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.dataset.aliasvaultIgnore = 'true';
@@ -370,7 +403,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
text-align: left;
`;
// Add focus styles
// Add focus styles.
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = '#2563eb';
searchInput.style.boxShadow = '0 0 0 2px rgba(37, 99, 235, 0.2)';
@@ -381,7 +414,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
searchInput.style.boxShadow = 'none';
});
// Handle search input
// Handle search input.
let searchTimeout: NodeJS.Timeout;
searchInput.addEventListener('input', () => {
@@ -654,8 +687,11 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
// Handle base64 image data
if (cred.Logo) {
try {
const base64Logo = base64Encode(cred.Logo as Uint8Array<ArrayBufferLike>);
imgElement.src = `data:image/x-icon;base64,${base64Logo}`;
const logoBytes = toUint8Array(cred.Logo);
const base64Logo = base64Encode(logoBytes);
// Detect image type from first few bytes
const mimeType = detectMimeType(logoBytes);
imgElement.src = `data:${mimeType};base64,${base64Logo}`;
} catch (error) {
console.error('Error setting logo:', error);
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
@@ -837,7 +873,7 @@ export async function createEditNamePopup(defaultName: string): Promise<string |
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999999999;
z-index: 999999995;
display: flex;
align-items: center;
justify-content: center;
@@ -1042,23 +1078,33 @@ export function openAutofillPopup(input: HTMLInputElement) : void {
});
}
/**
* Convert various binary data formats to Uint8Array
*/
function toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
if (buffer instanceof Uint8Array) {
return buffer;
}
if (Array.isArray(buffer)) {
return new Uint8Array(buffer);
}
const length = Object.keys(buffer).length;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = buffer[i];
}
return arr;
}
/**
* Base64 encode binary data.
*/
function base64Encode(buffer: Uint8Array | number[] | {[key: number]: number}): string | null {
try {
// Handle object with numeric keys
if (typeof buffer === 'object' && !Array.isArray(buffer) && !(buffer instanceof Uint8Array)) {
const length = Object.keys(buffer).length;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = buffer[i];
}
buffer = arr;
}
// Convert to array if Uint8Array
const arr = Array.from(buffer as Uint8Array | number[]);
const arr = Array.from(toUint8Array(buffer));
return btoa(arr.reduce((data, byte) => data + String.fromCharCode(byte), ''));
} catch (error) {
console.error('Error encoding to base64:', error);
@@ -1115,3 +1161,42 @@ async function getFaviconBytes(document: Document): Promise<Uint8Array | null> {
return null; // Return null if no favicon could be downloaded
}
/**
* Detect MIME type from file signature (magic numbers)
*/
function detectMimeType(bytes: Uint8Array): string {
/**
* Check if the file is an SVG file.
*/
const isSvg = () : boolean => {
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
return header.includes('<?xml') || header.includes('<svg');
};
/**
* Check if the file is an ICO file.
*/
const isIco = () : boolean => {
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
};
/**
* Check if the file is an PNG file.
*/
const isPng = () : boolean => {
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
};
if (isSvg()) {
return 'image/svg+xml';
}
if (isIco()) {
return 'image/x-icon';
}
if (isPng()) {
return 'image/png';
}
return 'image/x-icon';
}

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.1';
public static readonly VERSION = '0.12.3';
/**
* 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

@@ -19,13 +19,9 @@ export class WebApiService {
/**
* Constructor for the WebApiService class.
*
* @param {Function} handleLogout - Function to handle logout.
* @param {Function} authContextLogout - Function to handle logout.
*/
public constructor(
private readonly handleLogout: () => void
) {
// Remove initialization of baseUrl
}
public constructor(private readonly authContextLogout: (statusError: string | null) => void) { }
/**
* Get the base URL for the API from settings.
@@ -83,7 +79,7 @@ export class WebApiService {
return parseJson ? retryResponse.json() : retryResponse as unknown as T;
} else {
this.handleLogout();
this.authContextLogout(null);
throw new Error('Session expired');
}
}
@@ -110,11 +106,13 @@ export class WebApiService {
try {
const baseUrl = await this.getBaseUrl();
const response = await fetch(`${baseUrl}Auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Ignore-Failure': 'true',
'X-AliasVault-Client': `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`,
},
body: JSON.stringify({
token: await this.getAccessToken(),
@@ -130,7 +128,7 @@ export class WebApiService {
this.updateTokens(tokenResponse.token, tokenResponse.refreshToken);
return tokenResponse.token;
} catch {
this.handleLogout();
this.authContextLogout('Your session has expired. Please login again.');
return null;
}
}
@@ -150,7 +148,7 @@ export class WebApiService {
const response = await this.fetch<Response>(endpoint, {
method: 'GET',
headers: {
'Accept': 'application/octet-stream'
'Accept': 'application/octet-stream',
}
}, false);
@@ -201,18 +199,26 @@ export class WebApiService {
}
/**
* Logout and revoke tokens via WebApi.
* Logout and revoke tokens via WebApi and remove local storage tokens via AuthContext.
*/
public async logout(): Promise<void> {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
return;
public async logout(statusError: string | null = null): Promise<void> {
// Logout and revoke tokens via WebApi.
try {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
return;
}
await this.post('Auth/revoke', {
token: await this.getAccessToken(),
refreshToken: refreshToken,
}, false);
} catch (err) {
console.error('WebApi logout error:', err);
}
await this.post('Auth/revoke', {
token: await this.getAccessToken(),
refreshToken: refreshToken,
}, false);
// Logout and remove tokens from local storage via AuthContext.
this.authContextLogout(statusError);
}
/**
@@ -312,7 +318,7 @@ export class WebApiService {
/**
* When the reader has finished loading, convert the result to a Base64 string.
*/
reader.onloadend = () : void => {
reader.onloadend = (): void => {
const result = reader.result;
if (typeof result === 'string') {
resolve(result.split(',')[1]); // Remove the data URL prefix
@@ -324,7 +330,7 @@ export class WebApiService {
/**
* If the reader encounters an error, reject the promise with a proper Error object.
*/
reader.onerror = () : void => {
reader.onerror = (): void => {
reject(new Error('Failed to read blob as Data URL'));
};
reader.readAsDataURL(blob);

View File

@@ -5,6 +5,7 @@ export type FieldPatterns = {
username: string[];
firstName: string[];
lastName: string[];
fullName: string[];
email: string[];
emailConfirm: string[];
password: string[];
@@ -29,8 +30,9 @@ export type GenderOptionPatterns = {
*/
export const EnglishFieldPatterns: FieldPatterns = {
username: ['username', 'login', 'identifier', 'user'],
firstName: ['firstname', 'first-name', 'fname', 'name', 'given-name'],
lastName: ['lastname', 'last-name', 'lname', 'surname', 'family-name'],
fullName: ['fullname', 'full-name', 'full name'],
firstName: ['firstname', 'first-name', 'first_name', 'fname', 'name', 'given-name'],
lastName: ['lastname', 'last-name', 'last_name', 'lname', 'surname', 'family-name'],
email: ['email', 'mail', 'emailaddress'],
emailConfirm: ['confirm', 'verification', 'repeat', 'retype', 'verify'],
password: ['password', 'pwd', 'pass'],
@@ -55,6 +57,7 @@ export const EnglishGenderOptionPatterns: GenderOptionPatterns = {
*/
export const DutchFieldPatterns: FieldPatterns = {
username: ['gebruikersnaam', 'gebruiker', 'login', 'identifier'],
fullName: ['volledige naam'],
firstName: ['voornaam', 'naam'],
lastName: ['achternaam'],
email: ['e-mailadres', 'e-mail'],
@@ -81,6 +84,7 @@ export const DutchGenderOptionPatterns: GenderOptionPatterns = {
*/
export const CombinedFieldPatterns: FieldPatterns = {
username: [...new Set([...EnglishFieldPatterns.username, ...DutchFieldPatterns.username])],
fullName: [...new Set([...EnglishFieldPatterns.fullName, ...DutchFieldPatterns.fullName])],
firstName: [...new Set([...EnglishFieldPatterns.firstName, ...DutchFieldPatterns.firstName])],
lastName: [...new Set([...EnglishFieldPatterns.lastName, ...DutchFieldPatterns.lastName])],
/**

View File

@@ -101,15 +101,33 @@ export class FormDetector {
}
}
// Check for parent label
// Check for parent label and table cell structure
let currentElement = input;
for (let i = 0; i < 3; i++) {
// Check for parent label
const parentLabel = currentElement.closest('label');
if (parentLabel) {
attributes.push(parentLabel.textContent?.toLowerCase() ?? '');
break;
}
// Check for table cell structure
const parentTd = currentElement.closest('td');
if (parentTd) {
// Get the parent row
const parentTr = parentTd.closest('tr');
if (parentTr) {
// Check all sibling cells in the row
const siblingTds = parentTr.querySelectorAll('td');
for (const td of siblingTds) {
if (td !== parentTd) { // Skip the cell containing the input
attributes.push(td.textContent?.toLowerCase() ?? '');
}
}
}
break; // Found table structure, no need to continue up the tree
}
if (currentElement.parentElement) {
currentElement = currentElement.parentElement as HTMLInputElement;
} else {
@@ -410,6 +428,11 @@ export class FormDetector {
detectedFields.push(usernameField);
}
const fullNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.fullName, ['text'], detectedFields);
if (fullNameField) {
detectedFields.push(fullNameField);
}
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], detectedFields);
if (firstNameField) {
detectedFields.push(firstNameField);
@@ -446,6 +469,7 @@ export class FormDetector {
usernameField,
passwordField: passwordFields.primary,
passwordConfirmField: passwordFields.confirm,
fullNameField,
firstNameField,
lastNameField,
birthdateField,

View File

@@ -44,6 +44,27 @@ describe('FormDetector English tests', () => {
testField(FormField.LastName, 'fbclc_lName', htmlFile);
});
describe('English registration form 5 detection', () => {
const htmlFile = 'en-registration-form5.html';
testField(FormField.Username, 'aliasvault-input-7owmnahd9', htmlFile);
testField(FormField.Password, 'aliasvault-input-ienw3qgxv', htmlFile);
});
describe('English registration form 6 detection', () => {
const htmlFile = 'en-registration-form6.html';
testField(FormField.FirstName, 'id_first_name', htmlFile);
testField(FormField.LastName, 'id_last_name', htmlFile);
});
describe('English registration form 7 detection', () => {
const htmlFile = 'en-registration-form7.html';
testField(FormField.FullName, 'form-group--2', htmlFile);
testField(FormField.Email, 'form-group--4', htmlFile);
});
describe('English email form 1 detection', () => {
const htmlFile = 'en-email-form1.html';

View File

@@ -3,12 +3,13 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { it, expect } from 'vitest';
import { JSDOM } from 'jsdom';
import { LoginForm } from '../types/FormFields';
import { FormFields } from '../types/FormFields';
export enum FormField {
Username = 'username',
FirstName = 'firstName',
LastName = 'lastName',
FullName = 'fullName',
Email = 'email',
EmailConfirm = 'emailConfirm',
Password = 'password',
@@ -97,7 +98,7 @@ const loadTestHtml = (filename: string): string => {
/**
* Set up a form detection test.
*/
const setupFormTest = (htmlFile: string, focusedElementId: string) : { document: Document, result: LoginForm } => {
const setupFormTest = (htmlFile: string, focusedElementId: string) : { document: Document, result: FormFields | null } => {
const html = loadTestHtml(htmlFile);
const dom = new JSDOM(html, {
url: 'http://localhost',

View File

@@ -0,0 +1,6 @@
<html lang="en"><head><meta name="referrer" content="origin"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="icon" href="y18.svg"><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style></head><body><b>Login</b><br><br>
<form action="login" method="post"><input type="hidden" name="goto" value="news"><table border="0"><tbody><tr><td>username:</td><td><input type="text" name="acct" size="20" autocorrect="off" spellcheck="false" autocapitalize="off" autofocus="true" id="aliasvault-input-xahcz4tlf" autocomplete="false"></td></tr><tr><td>password:</td><td><input type="password" name="pw" size="20"></td></tr></tbody></table><br>
<input type="submit" value="login"></form><a href="forgot">Forgot your password?</a><br><br>
<b>Create Account</b><br><br>
<form action="login" method="post"><input type="hidden" name="goto" value="news"><input type="hidden" name="creating" value="t"><table border="0"><tbody><tr><td>username:</td><td><input type="text" name="acct" size="20" autocorrect="off" spellcheck="false" autocapitalize="off" id="aliasvault-input-7owmnahd9" autocomplete="false"></td></tr><tr><td>password:</td><td><input type="password" name="pw" size="20" id="aliasvault-input-ienw3qgxv" autocomplete="false"></td></tr></tbody></table><br>
<input type="submit" value="create account"></form></body></html>

View File

@@ -0,0 +1,26 @@
<form id="registerUser" action="/accounts/register/" method="post" class="register-form">
<input type="hidden" name="csrfmiddlewaretoken" value="U1l5CeQKuQfYceu7DCO2yx1XXFtYXTMYn6ZodGBCz9ruC5XHc56gQDbk7qKUp79d">
<input type="hidden" name="captcha" class="g-recaptcha" required_score="0.1" data-sitekey="6LckpJAUAAAAAFx3Ywv8kTCIusy2spXnPN27HYFE" id="id_captcha" data-widget-uuid="f91db472574f45898e363c64c961d2e7" data-callback="onSubmit_f91db472574f45898e363c64c961d2e7" data-size="normal" style="">
<div id="formRegisterInputs"><div class="row custom-col-12"><div class="col-12 col-md-12"><div class="form-group"><label for="id_first_name">First name<span>*</span></label> <input type="text" placeholder="James" id="id_first_name" name="first_name" required="required" value="" autofocus="autofocus" class="form-control" autocomplete="false"></div></div> <div class="col-12 col-md-12"><div class="form-group"><label for="id_last_name">Last name<span>*</span></label> <input type="text" placeholder="Davies" id="id_last_name" name="last_name" required="required" value="" class="form-control"></div></div></div> <div id="ue_emailRegister"><div class="form-group"><label for="id_email">Email<span>*</span></label> <input type="email" placeholder="jamesdavies@gmail.com" id="id_email" name="email" required="required" value="" autocomplete="off" class="form-control"></div></div> <div><div class="form-group"><div class="custom-password"><label for="id_password1">Password<span>*</span></label> <input placeholder="Password" name="password1" id="id_password1" required="required" type="password" class="form-control"> <i class="fas fa-eye"></i></div></div> <div class="custom-regex-condition"><p class="uncheck">12 characters</p> <p class="uncheck">lowercase letter (a-z)</p> <p class="uncheck">uppercase letter (A-Z)</p> <p class="uncheck">one digit (0-9)</p> <p class="uncheck">one special character (ex. &amp;%$#@!+_&lt;,&gt;:;)</p></div> <input type="hidden" placeholder="Password" id="id_password1" name="password1" required="required" value="?J@J$RNILhW{P6K.L&gt;" class="form-control"></div> <button type="submit" data-action="createAccount" class="btn btn-primary w-100"><span class="text-white">Sign up free</span> <div class="text-white" style="width: 40px; height: 25px; overflow: hidden; margin: 0px auto; display: none;"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 200 200" width="200" height="200" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px); content-visibility: visible;"><defs><clipPath id="__lottie_element_2"><rect width="200" height="200" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_2)"><g transform="matrix(1,0,0,1,68.26899719238281,101.01899719238281)" opacity="1" style="display: block;"><g opacity="1" transform="matrix(1,0,0,1,3.4809999465942383,-1.0190000534057617)"><path fill="rgb(255,255,255)" fill-opacity="1" d=" M0,-7.481500148773193 C4.129039764404297,-7.481500148773193 7.481500148773193,-4.129039764404297 7.481500148773193,0 C7.481500148773193,4.129039764404297 4.129039764404297,7.481500148773193 0,7.481500148773193 C-4.129039764404297,7.481500148773193 -7.481500148773193,4.129039764404297 -7.481500148773193,0 C-7.481500148773193,-4.129039764404297 -4.129039764404297,-7.481500148773193 0,-7.481500148773193z"></path></g></g><g transform="matrix(1,0,0,1,96.39399719238281,101.01899719238281)" opacity="0.2580833333333216" style="display: block;"><g opacity="1" transform="matrix(1,0,0,1,3.4809999465942383,-1.0190000534057617)"><path fill="rgb(255,255,255)" fill-opacity="1" d=" M0,-7.481500148773193 C4.129039764404297,-7.481500148773193 7.481500148773193,-4.129039764404297 7.481500148773193,0 C7.481500148773193,4.129039764404297 4.129039764404297,7.481500148773193 0,7.481500148773193 C-4.129039764404297,7.481500148773193 -7.481500148773193,4.129039764404297 -7.481500148773193,0 C-7.481500148773193,-4.129039764404297 -4.129039764404297,-7.481500148773193 0,-7.481500148773193z"></path></g></g><g transform="matrix(1,0,0,1,124.51899719238281,101.01899719238281)" opacity="0.13108333333332084" style="display: none;"><g opacity="1" transform="matrix(1,0,0,1,3.4809999465942383,-1.0190000534057617)"><path fill="rgb(255,255,255)" fill-opacity="1" d=" M0,-7.481500148773193 C4.129039764404297,-7.481500148773193 7.481500148773193,-4.129039764404297 7.481500148773193,0 C7.481500148773193,4.129039764404297 4.129039764404297,7.481500148773193 0,7.481500148773193 C-4.129039764404297,7.481500148773193 -7.481500148773193,4.129039764404297 -7.481500148773193,0 C-7.481500148773193,-4.129039764404297 -4.129039764404297,-7.481500148773193 0,-7.481500148773193z"></path></g></g></g></svg></div></button></div>
<fieldset class="form-group mb-0 mt-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="subscribe" id="id_subscribe">
<label for="id_subscribe" class="form-check-label">
I'd like to get useful tips, inspiration, and offers via email (you can unsubscribe at any time).</label>
</div>
</fieldset>
<span class="mt-4 mb-3" data-hr-text="OR"></span>
<p class="custom-p">By creating account I agree with</p>
</form>

View File

@@ -0,0 +1 @@
<form data-purpose="code-generation-form" data-gtm-form-interact-id="0"><div class="auth-form-row--small--Byo8R"><div class="ud-compact-form-group ud-form-group"><div class="ud-compact-form-control-container"><input aria-invalid="false" required="" name="full-name" id="form-group--2" type="text" class="ud-text-input ud-text-input-medium ud-text-sm ud-compact-form-control" value="" autocomplete="false" data-gtm-form-interact-field-id="1"><label for="form-group--2" class="ud-form-label ud-heading-sm"><span class="ud-compact-form-label-content"><span class="ud-compact-form-label-text">Full name</span></span></label></div></div></div><div class="auth-form-row--small--Byo8R"><div><div class="ud-compact-form-group ud-form-group"><div class="ud-compact-form-control-container"><input aria-invalid="false" name="email" minlength="7" maxlength="77" id="form-group--4" type="email" class="ud-text-input ud-text-input-medium ud-text-sm ud-compact-form-control" value="" data-gtm-form-interact-field-id="0" autocomplete="false"><label for="form-group--4" class="ud-form-label ud-heading-sm"><span class="ud-compact-form-label-content"><span class="ud-compact-form-label-text">Email</span></span></label></div></div></div></div><button type="submit" class="ud-btn ud-btn-large ud-btn-brand ud-heading-md passwordless-auth-mx-code-generation-form--submit-button--2vOvZ"><svg aria-hidden="true" focusable="false" class="ud-icon ud-icon-medium"><use xlink:href="#icon-email"></use></svg>Continue with email</button></form>

View File

@@ -7,6 +7,7 @@ export type FormFields = {
passwordConfirmField: HTMLInputElement | null;
// Identity fields
fullNameField: HTMLInputElement | null;
firstNameField: HTMLInputElement | null;
lastNameField: HTMLInputElement | null;

View File

@@ -56,3 +56,5 @@ The following websites have been known to cause issues in the past (but should b
| --- | --- |
| https://www.paprika-shopping.nl/nieuwsbrief/newsletter-register-landing.html | Popup CSS style conflicts |
| https://bloshing.com/inschrijven-nieuwsbrief | Popup CSS style conflicts |
| https://gamefaqs.gamespot.com/user | Popup buttons not working |
| https://news.ycombinator.com/login?goto=news | Popup and client favicon not showing due to SVG format |

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

@@ -26,6 +26,9 @@ else
[Parameter]
public bool Padding { get; set; }
/// <summary>
/// The data URL of the favicon.
/// </summary>
private string? _faviconDataUrl;
/// <inheritdoc />
@@ -33,8 +36,46 @@ else
{
if (FaviconBytes is not null)
{
string mimeType = DetectMimeType(FaviconBytes);
string base64String = Convert.ToBase64String(FaviconBytes);
_faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
_faviconDataUrl = $"data:{mimeType};base64,{base64String}";
}
}
/// <summary>
/// Detect the mime type of the favicon.
/// </summary>
/// <param name="bytes">The bytes of the favicon.</param>
/// <returns>The mime type of the favicon.</returns>
private static string DetectMimeType(byte[] bytes)
{
// Check for SVG.
if (bytes.Length >= 5)
{
string header = System.Text.Encoding.ASCII.GetString(bytes.Take(5).ToArray()).ToLower();
if (header.Contains("<?xml") || header.Contains("<svg"))
{
return "image/svg+xml";
}
}
// Check for ICO.
if (bytes.Length >= 4 &&
bytes[0] == 0x00 && bytes[1] == 0x00 &&
bytes[2] == 0x01 && bytes[3] == 0x00)
{
return "image/x-icon";
}
// Check for PNG.
if (bytes.Length >= 4 &&
bytes[0] == 0x89 && bytes[1] == 0x50 &&
bytes[2] == 0x4E && bytes[3] == 0x47)
{
return "image/png";
}
// Default to x-icon if unknown.
return "image/x-icon";
}
}

View File

@@ -1,6 +1,6 @@
@if (IsVisible)
{
<div class="loading fixed inset-0 w-full h-full z-50 bg-gray-200 !m-0 !p-0 dark:bg-gray-500">
<div class="loading fixed inset-0 w-full h-full z-50 bg-gray-200 !m-0 !p-0 dark:bg-gray-500" style="z-index: 2147483641 !important;">
<div class="spinner">
<div class="rect1 bg-gray-900 dark:bg-white"></div>
<div class="rect2 bg-gray-900 dark:bg-white"></div>

View File

@@ -72,6 +72,14 @@
Vault settings
</NavLink>
</li>
<li class="border-t border-b border-gray-100 dark:border-gray-600">
<NavLink href="/settings/browser-extensions" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Install Browser Extension
<span class="ml-2 inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium rounded bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200">
NEW
</span>
</NavLink>
</li>
<li>
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="w-full text-start py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">
Toggle dark mode

View File

@@ -0,0 +1,178 @@
@page "/settings/browser-extensions"
@inherits MainBase
@inject IJSRuntime JsRuntime
@inject ILogger<BrowserExtensions> Logger
<LayoutPageTitle>Install Browser Extension</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Install Browser Extension"
Description="Install browser extensions to automatically fill credentials on websites.">
</PageHeader>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Available Extensions</h3>
<p class="text-gray-600 dark:text-gray-400">
The AliasVault browser extension allows you to:
</p>
<ul class="list-disc list-inside mt-2 space-y-1 text-gray-600 dark:text-gray-400">
<li>Autofill existing credentials on any website</li>
<li>Generate new aliases during registration</li>
<li>Access received emails on all of your aliases</li>
<li>View your aliases and identities</li>
</ul>
</div>
@if (CurrentBrowser != BrowserType.Unknown)
{
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Recommended for Your Browser</h3>
<div class="p-4 border rounded-lg dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20 border-blue-200">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center">
<img src="@GetBrowserIcon(CurrentBrowser)" alt="@CurrentBrowser" class="w-8 h-8 mr-3">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">@GetBrowserName(CurrentBrowser)</h4>
</div>
@if (CurrentBrowser == BrowserType.Chrome)
{
<a href="https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj"
target="_blank"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900">
Install for Chrome
</a>
}
else
{
<p class="text-sm text-blue-800 dark:text-blue-400">
Support for @GetBrowserName(CurrentBrowser) is coming soon! For now, you can use our Chrome extension.
</p>
}
</div>
</div>
</div>
}
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Other Browsers</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach (var browser in Enum.GetValues<BrowserType>().Where(b => b != BrowserType.Unknown && b != CurrentBrowser))
{
<div class="p-4 border rounded-lg dark:border-gray-700">
<div class="flex items-center mb-4">
<img src="@GetBrowserIcon(browser)" alt="@GetBrowserName(browser)" class="w-8 h-8 mr-3">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">@GetBrowserName(browser)</h4>
</div>
@if (browser == BrowserType.Chrome)
{
<a href="https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj"
target="_blank"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900">
Install for Chrome
</a>
}
else
{
<span class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 rounded-lg dark:text-gray-400 dark:bg-gray-800">
Coming soon
</span>
}
</div>
}
</div>
</div>
</div>
@code {
/// <summary>
/// The type of browser.
/// </summary>
private enum BrowserType
{
Unknown,
Firefox,
Chrome,
Safari,
Edge,
Brave
}
/// <summary>
/// The current browser of the user.
/// </summary>
private BrowserType CurrentBrowser { get; set; }
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Install Browser Extension" });
try
{
CurrentBrowser = await DetermineBrowser();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error determining browser type");
}
}
/// <summary>
/// Determine current browser.
/// </summary>
/// <returns>Browser type enum value.</returns>
private async Task<BrowserType> DetermineBrowser()
{
try
{
// First check if it's Brave.
var isBrave = await JsRuntime.InvokeAsync<bool>("eval", "navigator.brave?.isBrave() || false");
if (isBrave)
{
return BrowserType.Brave;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error checking for Brave browser");
}
var userAgent = await JsRuntime.InvokeAsync<string>("eval", "navigator.userAgent");
return userAgent.ToLower() switch
{
var x when x.Contains("firefox") => BrowserType.Firefox,
var x when x.Contains("chrome") && !x.Contains("edg") => BrowserType.Chrome,
var x when x.Contains("safari") && !x.Contains("chrome") => BrowserType.Safari,
var x when x.Contains("edg") => BrowserType.Edge,
_ => BrowserType.Unknown
};
}
/// <summary>
/// Gets the browser icon path.
/// </summary>
private static string GetBrowserIcon(BrowserType browser) => browser switch
{
BrowserType.Firefox => "/img/browser-icons/firefox.svg",
BrowserType.Chrome => "/img/browser-icons/chrome.svg",
BrowserType.Safari => "/img/browser-icons/safari.svg",
BrowserType.Edge => "/img/browser-icons/edge.svg",
BrowserType.Brave => "/img/browser-icons/brave.svg",
_ => string.Empty
};
/// <summary>
/// Gets the browser display name.
/// </summary>
private static string GetBrowserName(BrowserType browser) => browser switch
{
BrowserType.Firefox => "Firefox",
BrowserType.Chrome => "Google Chrome",
BrowserType.Safari => "Safari",
BrowserType.Edge => "Microsoft Edge",
BrowserType.Brave => "Brave",
_ => string.Empty
};
}

View File

@@ -1186,6 +1186,12 @@ video {
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1331,11 +1337,21 @@ video {
border-top-width: 2px;
}
.border-blue-200 {
--tw-border-opacity: 1;
border-color: rgb(191 219 254 / var(--tw-border-opacity));
}
.border-blue-700 {
--tw-border-opacity: 1;
border-color: rgb(29 78 216 / var(--tw-border-opacity));
}
.border-gray-100 {
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity));
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@@ -1891,6 +1907,11 @@ video {
color: rgb(184 112 47 / var(--tw-text-opacity));
}
.text-primary-800 {
--tw-text-opacity: 1;
color: rgb(154 93 38 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
@@ -2242,6 +2263,11 @@ video {
--tw-ring-color: rgb(134 239 172 / var(--tw-ring-opacity));
}
.focus\:ring-primary-200:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(251 203 116 / var(--tw-ring-opacity));
}
.focus\:ring-primary-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(248 185 99 / var(--tw-ring-opacity));
@@ -2351,6 +2377,10 @@ video {
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}
.dark\:bg-blue-900\/20:is(.dark *) {
background-color: rgb(30 58 138 / 0.2);
}
.dark\:bg-gray-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -2396,6 +2426,11 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.dark\:bg-primary-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(123 74 30 / var(--tw-bg-opacity));
}
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
@@ -2678,6 +2713,11 @@ video {
--tw-ring-color: rgb(154 93 38 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-primary-900:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(123 74 30 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-red-800:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(153 27 27 / var(--tw-ring-opacity));
@@ -2729,6 +2769,14 @@ video {
flex-direction: row;
}
.sm\:items-center {
align-items: center;
}
.sm\:justify-between {
justify-content: space-between;
}
.sm\:rounded-lg {
border-radius: 0.5rem;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2770 2770"><linearGradient id="a" y1="51%" y2="51%"><stop offset=".4" stop-color="#f50"/><stop offset=".6" stop-color="#ff2000"/></linearGradient><linearGradient id="b" x1="2%" y1="51%" y2="51%"><stop offset="0" stop-color="#ff452a"/><stop offset="1" stop-color="#ff2000"/></linearGradient><path fill="url(#a)" d="M2395 723l60-147-170-176c-92-92-288-38-288-38l-222-252H992L769 363s-196-53-288 37L311 575l60 147-75 218 250 953c52 204 87 283 234 387l457 310c44 27 98 74 147 74s103-47 147-74l457-310c147-104 182-183 234-387l250-953z"/><path fill="#fff" d="M1935 524s287 347 287 420c0 75-36 94-72 133l-215 230c-20 20-63 54-38 113 25 60 60 134 20 210-40 77-110 128-155 120a820 820 0 01-190-90c-38-25-160-126-160-165s126-110 150-124c23-16 130-78 132-102s2-30-30-90-88-140-80-192c10-52 100-80 167-105l207-78c16-8 12-15-36-20-48-4-183-22-244-5s-163 43-173 57c-8 14-16 14-7 62l58 315c4 40 12 67-30 77-44 10-117 27-142 27s-99-17-142-27-35-37-30-77c4-40 48-268 57-315 10-48 1-48-7-62-10-14-113-40-174-57-60-17-196 1-244 6-48 4-52 10-36 20l207 77c66 25 158 53 167 105 10 53-47 132-80 192s-32 66-30 90 110 86 132 102c24 15 150 85 150 124s-119 140-159 165a820 820 0 01-190 90c-45 8-115-43-156-120-40-76-4-150 20-210 25-60-17-92-38-113l-215-230c-35-37-71-57-71-131s287-420 287-420l273 44c32 0 103-27 168-50 65-20 110-22 110-22s44 0 110 22 136 50 168 50c33 0 275-47 275-47zm-215 1328c18 10 7 32-10 44l-254 198c-20 20-52 50-73 50s-52-30-73-50a13200 13200 0 00-255-198c-16-12-27-33-10-44l150-80a870 870 0 01188-73c15 0 110 34 187 73l150 80z"/><path fill="url(#b)" d="M1999 363l-224-253H992L769 363s-196-53-288 37c0 0 260-23 350 123l276 47c32 0 103-27 168-50 65-20 110-22 110-22s44 0 110 22 136 50 168 50c33 0 275-47 275-47 90-146 350-123 350-123-92-92-288-38-288-38"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 276 276"><linearGradient id="a" x1="145" x2="34" y1="253" y2="61" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1e8e3e"/><stop offset="1" stop-color="#34a853"/></linearGradient><linearGradient id="b" x1="111" x2="222" y1="254" y2="62" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcc934"/><stop offset="1" stop-color="#fbbc04"/></linearGradient><linearGradient id="c" x1="17" x2="239" y1="80" y2="80" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d93025"/><stop offset="1" stop-color="#ea4335"/></linearGradient><circle cx="128" cy="128" r="64" fill="#fff"/><path fill="url(#a)" d="M96 183.4A63.7 63.7 0 0 1 72.6 160L17.2 64A128 128 0 0 0 128 256l55.4-96A64 64 0 0 1 96 183.4Z"/><path fill="url(#b)" d="M192 128a63.7 63.7 0 0 1-8.6 32L128 256A128 128 0 0 0 238.9 64h-111a64 64 0 0 1 64 64Z"/><circle cx="128" cy="128" r="52" fill="#1a73e8"/><path fill="url(#c)" d="M96 72.6a63.7 63.7 0 0 1 32-8.6h110.8a128 128 0 0 0-221.7 0l55.5 96A64 64 0 0 1 96 72.6Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 27600 27600"><linearGradient id="a" gradientUnits="userSpaceOnUse"/><linearGradient id="b" x1="6870" x2="24704" y1="18705" y2="18705" x:href="#a"><stop offset="0" stop-color="#0c59a4"/><stop offset="1" stop-color="#114a8b"/></linearGradient><linearGradient id="c" x1="16272" x2="5133" y1="10968" y2="23102" x:href="#a"><stop offset="0" stop-color="#1b9de2"/><stop offset=".16" stop-color="#1595df"/><stop offset=".67" stop-color="#0680d7"/><stop offset="1" stop-color="#0078d4"/></linearGradient><radialGradient id="d" cx="16720" cy="18747" r="9538" x:href="#a"><stop offset=".72" stop-opacity="0"/><stop offset=".95" stop-opacity=".53"/><stop offset="1"/></radialGradient><radialGradient id="e" cx="7130" cy="19866" r="14324" gradientTransform="matrix(.14843 -.98892 .79688 .1196 -8759 25542)" x:href="#a"><stop offset=".76" stop-opacity="0"/><stop offset=".95" stop-opacity=".5"/><stop offset="1"/></radialGradient><radialGradient id="f" cx="2523" cy="4680" r="20243" gradientTransform="matrix(-.03715 .99931 -2.12836 -.07913 13579 3530)" x:href="#a"><stop offset="0" stop-color="#35c1f1"/><stop offset=".11" stop-color="#34c1ed"/><stop offset=".23" stop-color="#2fc2df"/><stop offset=".31" stop-color="#2bc3d2"/><stop offset=".67" stop-color="#36c752"/></radialGradient><radialGradient id="g" cx="24247" cy="7758" r="9734" gradientTransform="matrix(.28109 .95968 -.78353 .22949 24510 -16292)" x:href="#a"><stop offset="0" stop-color="#66eb6e"/><stop offset="1" stop-color="#66eb6e" stop-opacity="0"/></radialGradient><path id="h" d="M24105 20053a9345 9345 0 01-1053 472 10202 10202 0 01-3590 646c-4732 0-8855-3255-8855-7432 0-1175 680-2193 1643-2729-4280 180-5380 4640-5380 7253 0 7387 6810 8137 8276 8137 791 0 1984-230 2704-456l130-44a12834 12834 0 006660-5282c220-350-168-757-535-565z"/><path id="i" d="M11571 25141a7913 7913 0 01-2273-2137 8145 8145 0 01-1514-4740 8093 8093 0 013093-6395 8082 8082 0 011373-859c312-148 846-414 1554-404a3236 3236 0 012569 1297 3184 3184 0 01636 1866c0-21 2446-7960-8005-7960-4390 0-8004 4166-8004 7820 0 2319 538 4170 1212 5604a12833 12833 0 007684 6757 12795 12795 0 003908 610c1414 0 2774-233 4045-656a7575 7575 0 01-6278-803z"/><path id="j" d="M16231 15886c-80 105-330 250-330 566 0 260 170 512 472 723 1438 1003 4149 868 4156 868a5954 5954 0 003027-839 6147 6147 0 001133-850 6180 6180 0 001910-4437c26-2242-796-3732-1133-4392-2120-4141-6694-6525-11668-6525-7011 0-12703 5635-12798 12620 47-3654 3679-6605 7996-6605 350 0 2346 34 4200 1007 1634 858 2490 1894 3086 2921 618 1067 728 2415 728 2952s-271 1333-780 1990z"/><use fill="url(#b)" x:href="#h"/><use fill="url(#d)" opacity=".35" x:href="#h"/><use fill="url(#c)" x:href="#i"/><use fill="url(#e)" opacity=".4" x:href="#i"/><use fill="url(#f)" x:href="#j"/><use fill="url(#g)" x:href="#j"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1090 1090"><linearGradient id="a" x1="461" x2="461" y1="59" y2="1033" gradientUnits="userSpaceOnUse"><stop offset=".6" stop-color="#ff1b2d"/><stop offset="1" stop-color="#a70014"/></linearGradient><linearGradient id="b" x1="714" x2="714" y1="116" y2="978" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9c0000"/><stop offset=".7" stop-color="#ff4b4b"/></linearGradient><path fill="url(#a)" d="M545 42.5a502.5 502.5 0 10334.9 877.1 362.4 362.4 0 01-201.4 61.5c-119.7 0-226.8-59.4-299-153-55.6-65.6-91.5-162.5-94-271.3V533c2.5-108.8 38.4-205.8 94-271.3 72-93.6 179.3-153 299-153 73.6 0 142.5 22.5 201.4 61.6a500.8 500.8 0 00-333-127.9h-2z"/><path fill="url(#b)" d="M379.6 261.8c46-54.4 105.7-87.3 170.7-87.3 146.3 0 265 166 265 370.4 0 204.6-118.6 370.4-265 370.4-65 0-124.6-32.8-170.7-87.2 72 93.6 179.2 153 299 153A363 363 0 00880 919.6 501 501 0 001047.5 545a501.1 501.1 0 00-167.6-374.6 362.4 362.4 0 00-201.4-61.5c-119.7 0-226.8 59.4-299 153"/></svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

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 = 3;
/// <summary>
/// Gets a dictionary of minimum supported client versions that the WebApi supports.