From 5750eef24898ba86314f230c0d193a5d4557fcc2 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 26 Feb 2025 20:51:36 +0100 Subject: [PATCH 1/8] Add client header to webapi token refresh call (#618) --- browser-extensions/chrome/src/shared/WebApiService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/browser-extensions/chrome/src/shared/WebApiService.ts b/browser-extensions/chrome/src/shared/WebApiService.ts index c0af80d88..7a5adb1a6 100644 --- a/browser-extensions/chrome/src/shared/WebApiService.ts +++ b/browser-extensions/chrome/src/shared/WebApiService.ts @@ -106,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(), @@ -146,7 +148,7 @@ export class WebApiService { const response = await this.fetch(endpoint, { method: 'GET', headers: { - 'Accept': 'application/octet-stream' + 'Accept': 'application/octet-stream', } }, false); From 50817b65d3e658bdbeb5e2c00f83af996c916333 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 15:24:54 +0100 Subject: [PATCH 2/8] Make popup create button more robust (#622) --- .../chrome/src/contentScript/Form.ts | 2 +- .../chrome/src/contentScript/Popup.ts | 42 +++++++++++++++---- docs/misc/dev/browser-extensions.md | 1 + 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/browser-extensions/chrome/src/contentScript/Form.ts b/browser-extensions/chrome/src/contentScript/Form.ts index 0b550d6d0..2aa81d7e5 100644 --- a/browser-extensions/chrome/src/contentScript/Form.ts +++ b/browser-extensions/chrome/src/contentScript/Form.ts @@ -354,7 +354,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; diff --git a/browser-extensions/chrome/src/contentScript/Popup.ts b/browser-extensions/chrome/src/contentScript/Popup.ts index 321e0f251..e54e4b727 100644 --- a/browser-extensions/chrome/src/contentScript/Popup.ts +++ b/browser-extensions/chrome/src/contentScript/Popup.ts @@ -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,12 @@ 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) => { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + // Determine service name based on conditions let suggestedName = document.title; @@ -331,7 +336,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden }; chrome.runtime.sendMessage({ type: 'CREATE_IDENTITY', credential }, () => { - // Refresh the popup to show new identity + // Refresh the popup to show new identity. openAutofillPopup(input); }); } catch (error) { @@ -345,9 +350,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 +398,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 +409,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', () => { @@ -837,7 +865,7 @@ export async function createEditNamePopup(defaultName: string): Promise Date: Thu, 27 Feb 2025 15:48:56 +0100 Subject: [PATCH 3/8] Update form detection and popup icon display (#622) --- .../chrome/src/contentScript/Popup.ts | 87 +++++++++++++++---- .../src/shared/formDetector/FormDetector.ts | 20 ++++- .../__tests__/FormDetector.en.test.ts | 7 ++ .../test-forms/en-registration-form5.html | 6 ++ docs/misc/dev/browser-extensions.md | 1 + 5 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html diff --git a/browser-extensions/chrome/src/contentScript/Popup.ts b/browser-extensions/chrome/src/contentScript/Popup.ts index e54e4b727..a310626de 100644 --- a/browser-extensions/chrome/src/contentScript/Popup.ts +++ b/browser-extensions/chrome/src/contentScript/Popup.ts @@ -228,8 +228,10 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden createButton.style.backgroundColor = isDarkMode() ? '#374151' : '#f3f4f6'; }); - // Handle create button click - const handleCreateClick = async (e: Event) => { + /** + * Handle create button click + */ + const handleCreateClick = async (e: Event) : Promise => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); @@ -682,8 +684,11 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement // Handle base64 image data if (cred.Logo) { try { - const base64Logo = base64Encode(cred.Logo as Uint8Array); - 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}`; @@ -1070,23 +1075,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); + } + + // Handle object with numeric keys + 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); @@ -1143,3 +1158,43 @@ async function getFaviconBytes(document: Document): Promise { 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(' { + 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'; + } + + // Default to x-icon if unknown + return 'image/x-icon'; +} diff --git a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts index 79fa12ffe..946cb12a3 100644 --- a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts +++ b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts @@ -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 { diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts index c694de033..a90656b4e 100644 --- a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts +++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts @@ -44,6 +44,13 @@ 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 email form 1 detection', () => { const htmlFile = 'en-email-form1.html'; diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html new file mode 100644 index 000000000..2e52e3cfd --- /dev/null +++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html @@ -0,0 +1,6 @@ +Login

+
username:
password:

+
Forgot your password?

+ Create Account

+
username:
password:

+
\ No newline at end of file diff --git a/docs/misc/dev/browser-extensions.md b/docs/misc/dev/browser-extensions.md index b189fd0d8..ef8e1eb5a 100644 --- a/docs/misc/dev/browser-extensions.md +++ b/docs/misc/dev/browser-extensions.md @@ -57,3 +57,4 @@ 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 logo not showing due to SVG format after identity creation | \ No newline at end of file From 6d33f99d62f1698c46e4a5ee58e5ce6bdf7e0856 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 16:11:23 +0100 Subject: [PATCH 4/8] Update favicon display in client to handle SVG (#622) --- docs/misc/dev/browser-extensions.md | 4 +- .../Credentials/DisplayFavicon.razor | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/misc/dev/browser-extensions.md b/docs/misc/dev/browser-extensions.md index ef8e1eb5a..0a96d2644 100644 --- a/docs/misc/dev/browser-extensions.md +++ b/docs/misc/dev/browser-extensions.md @@ -56,5 +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 logo not showing due to SVG format after identity creation | \ No newline at end of file +| 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 | diff --git a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor index a3186fc28..fe0237ede 100644 --- a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor +++ b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor @@ -26,6 +26,9 @@ else [Parameter] public bool Padding { get; set; } + /// + /// The data URL of the favicon. + /// private string? _faviconDataUrl; /// @@ -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}"; } } + + /// + /// Detect the mime type of the favicon. + /// + /// The bytes of the favicon. + /// The mime type of the favicon. + private 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("= 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"; + } } From 6d0352923ad6e7adff41f94b7e26b8b7a639bb7a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 16:39:33 +0100 Subject: [PATCH 5/8] Simplify main logout flow to use page redirect (#622) --- browser-extensions/chrome/src/app/App.tsx | 4 ++- .../src/app/components/Layout/UserMenu.tsx | 5 +-- .../chrome/src/app/context/DbContext.tsx | 1 + .../chrome/src/app/pages/Logout.tsx | 31 +++++++++++++++++++ .../chrome/src/shared/WebApiService.ts | 4 +-- 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 browser-extensions/chrome/src/app/pages/Logout.tsx diff --git a/browser-extensions/chrome/src/app/App.tsx b/browser-extensions/chrome/src/app/App.tsx index ca682ab0f..3fa389cf2 100644 --- a/browser-extensions/chrome/src/app/App.tsx +++ b/browser-extensions/chrome/src/app/App.tsx @@ -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: , showBackButton: false }, { path: '/emails/:id', element: , showBackButton: true, title: 'Email details' }, { path: '/settings', element: , showBackButton: false }, + { path: '/logout', element: , showBackButton: false }, ]; useEffect(() => { diff --git a/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx b/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx index d14752c62..f171ba087 100644 --- a/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx +++ b/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx @@ -47,10 +47,7 @@ export const UserMenu: React.FC = () => { */ const onLogout = async () : Promise => { 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(); }; diff --git a/browser-extensions/chrome/src/app/context/DbContext.tsx b/browser-extensions/chrome/src/app/context/DbContext.tsx index 81170725d..72a8f6a83 100644 --- a/browser-extensions/chrome/src/app/context/DbContext.tsx +++ b/browser-extensions/chrome/src/app/context/DbContext.tsx @@ -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; diff --git a/browser-extensions/chrome/src/app/pages/Logout.tsx b/browser-extensions/chrome/src/app/pages/Logout.tsx new file mode 100644 index 000000000..577becb09 --- /dev/null +++ b/browser-extensions/chrome/src/app/pages/Logout.tsx @@ -0,0 +1,31 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +/** + * Logout page. + */ +const Logout: React.FC = () => { + const authContext = useAuth(); + 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 => { + await authContext.logout(); + navigate('/'); + }; + + performLogout(); + }, [authContext, navigate]); + + // Return null since this is just a functional component that handles logout. + return null; +}; + +export default Logout; diff --git a/browser-extensions/chrome/src/shared/WebApiService.ts b/browser-extensions/chrome/src/shared/WebApiService.ts index 7a5adb1a6..f3725c3f1 100644 --- a/browser-extensions/chrome/src/shared/WebApiService.ts +++ b/browser-extensions/chrome/src/shared/WebApiService.ts @@ -310,7 +310,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 @@ -322,7 +322,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); From eb587e34964b78ebffba1435b3d141135498b88f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 16:54:54 +0100 Subject: [PATCH 6/8] Add webapi logout call to all places (#622) --- .../chrome/src/app/pages/CredentialsList.tsx | 12 ++++++++ .../chrome/src/app/pages/Logout.tsx | 11 ++++++-- .../chrome/src/app/pages/Unlock.tsx | 28 +++++-------------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx index 00f43bbf0..7e7880895 100644 --- a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx +++ b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx @@ -40,6 +40,12 @@ const CredentialsList: React.FC = () => { const statusResponse = await webApi.getStatus(); const statusError = webApi.validateStatusResponse(statusResponse); if (statusError !== null) { + try { + await webApi.logout(); + } catch (err) { + console.error('WebApi logout error:', err); + } + authContext.logout(statusError); return; } @@ -60,6 +66,12 @@ const CredentialsList: React.FC = () => { const vaultError = webApi.validateVaultResponse(vaultResponseJson); if (vaultError) { + try { + await webApi.logout(); + } catch (err) { + console.error('WebApi logout error:', err); + } + authContext.logout(vaultError); hideLoading(); return; diff --git a/browser-extensions/chrome/src/app/pages/Logout.tsx b/browser-extensions/chrome/src/app/pages/Logout.tsx index 577becb09..6f36049a1 100644 --- a/browser-extensions/chrome/src/app/pages/Logout.tsx +++ b/browser-extensions/chrome/src/app/pages/Logout.tsx @@ -1,14 +1,15 @@ 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. */ @@ -17,12 +18,18 @@ const Logout: React.FC = () => { * Perform logout via async method to ensure logout is completed before navigating to home page. */ const performLogout = async () : Promise => { + try { + await webApi.logout(); + } catch (err) { + console.error('WebApi logout error:', err); + } + await authContext.logout(); navigate('/'); }; performLogout(); - }, [authContext, navigate]); + }, [authContext, navigate, webApi]); // Return null since this is just a functional component that handles logout. return null; diff --git a/browser-extensions/chrome/src/app/pages/Unlock.tsx b/browser-extensions/chrome/src/app/pages/Unlock.tsx index 1d1972d11..8c8c27f6b 100644 --- a/browser-extensions/chrome/src/app/pages/Unlock.tsx +++ b/browser-extensions/chrome/src/app/pages/Unlock.tsx @@ -31,6 +31,12 @@ const Unlock: React.FC = () => { const statusResponse = await webApi.getStatus(); const statusError = webApi.validateStatusResponse(statusResponse); if (statusError !== null) { + try { + await webApi.logout(); + } catch (err) { + console.error('WebApi logout error:', err); + } + authContext.logout(statusError); } }; @@ -81,26 +87,6 @@ const Unlock: React.FC = () => { } }; - /** - * Handle logout - */ - const handleLogout = async () : Promise => { - 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 (
@@ -136,7 +122,7 @@ const Unlock: React.FC = () => {
- Switch accounts? Log out + Switch accounts? Log out
From bc161672934e4160d1bdf70bca47c806ae4806ce Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 17:19:33 +0100 Subject: [PATCH 7/8] Refactor (#622) --- .../chrome/src/contentScript/Popup.ts | 3 +- .../Credentials/DisplayFavicon.razor | 2 +- .../wwwroot/css/tailwind.css | 58 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/browser-extensions/chrome/src/contentScript/Popup.ts b/browser-extensions/chrome/src/contentScript/Popup.ts index a310626de..a7cc8a843 100644 --- a/browser-extensions/chrome/src/contentScript/Popup.ts +++ b/browser-extensions/chrome/src/contentScript/Popup.ts @@ -1087,12 +1087,12 @@ function toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): return new Uint8Array(buffer); } - // Handle object with numeric keys const length = Object.keys(buffer).length; const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { arr[i] = buffer[i]; } + return arr; } @@ -1195,6 +1195,5 @@ function detectMimeType(bytes: Uint8Array): string { return 'image/png'; } - // Default to x-icon if unknown return 'image/x-icon'; } diff --git a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor index fe0237ede..80959d9ee 100644 --- a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor +++ b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor @@ -47,7 +47,7 @@ else /// /// The bytes of the favicon. /// The mime type of the favicon. - private string DetectMimeType(byte[] bytes) + private static string DetectMimeType(byte[] bytes) { // Check for SVG. if (bytes.Length >= 5) diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index e27de19d9..970d18027 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -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,6 +1337,11 @@ video { border-top-width: 2px; } +.border-blue-300 { + --tw-border-opacity: 1; + border-color: rgb(147 197 253 / var(--tw-border-opacity)); +} + .border-blue-700 { --tw-border-opacity: 1; border-color: rgb(29 78 216 / var(--tw-border-opacity)); @@ -1376,6 +1387,16 @@ video { border-color: rgb(239 68 68 / var(--tw-border-opacity)); } +.border-blue-200 { + --tw-border-opacity: 1; + border-color: rgb(191 219 254 / var(--tw-border-opacity)); +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + .bg-amber-50 { --tw-bg-opacity: 1; background-color: rgb(255 251 235 / var(--tw-bg-opacity)); @@ -1713,6 +1734,10 @@ video { vertical-align: middle; } +.align-text-bottom { + vertical-align: text-bottom; +} + .font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; } @@ -1921,6 +1946,11 @@ video { color: rgb(133 77 14 / var(--tw-text-opacity)); } +.text-primary-800 { + --tw-text-opacity: 1; + color: rgb(154 93 38 / var(--tw-text-opacity)); +} + .opacity-0 { opacity: 0; } @@ -2242,6 +2272,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)); @@ -2296,6 +2331,11 @@ video { border-color: rgb(59 130 246 / var(--tw-border-opacity)); } +.dark\:border-blue-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(30 64 175 / var(--tw-border-opacity)); +} + .dark\:border-gray-400:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(156 163 175 / var(--tw-border-opacity)); @@ -2351,6 +2391,10 @@ video { background-color: rgb(30 58 138 / var(--tw-bg-opacity)); } +.dark\:bg-blue-900\/50:is(.dark *) { + background-color: rgb(30 58 138 / 0.5); +} + .dark\:bg-gray-500:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(107 114 128 / var(--tw-bg-opacity)); @@ -2430,6 +2474,15 @@ video { background-color: rgb(113 63 18 / var(--tw-bg-opacity)); } +.dark\:bg-blue-900\/20:is(.dark *) { + background-color: rgb(30 58 138 / 0.2); +} + +.dark\:bg-primary-900:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(123 74 30 / var(--tw-bg-opacity)); +} + .dark\:bg-opacity-80:is(.dark *) { --tw-bg-opacity: 0.8; } @@ -2678,6 +2731,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)); From 7253d1fee23726cbe321a7336d0775c069c2c13a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 27 Feb 2025 17:35:28 +0100 Subject: [PATCH 8/8] Do all logout actions via webapi which calls authcontext too (#622) --- .../chrome/src/app/context/WebApiContext.tsx | 8 ++++- .../chrome/src/app/pages/CredentialsList.tsx | 20 ++--------- .../chrome/src/app/pages/Logout.tsx | 8 +---- .../chrome/src/app/pages/Unlock.tsx | 8 +---- .../chrome/src/shared/WebApiService.ts | 34 ++++++++++++------- 5 files changed, 33 insertions(+), 45 deletions(-) diff --git a/browser-extensions/chrome/src/app/context/WebApiContext.tsx b/browser-extensions/chrome/src/app/context/WebApiContext.tsx index 21e813f9c..65f7b6e92 100644 --- a/browser-extensions/chrome/src/app/context/WebApiContext.tsx +++ b/browser-extensions/chrome/src/app/context/WebApiContext.tsx @@ -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]); diff --git a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx index 7e7880895..56549ce86 100644 --- a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx +++ b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx @@ -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,13 +38,7 @@ const CredentialsList: React.FC = () => { const statusResponse = await webApi.getStatus(); const statusError = webApi.validateStatusResponse(statusResponse); if (statusError !== null) { - try { - await webApi.logout(); - } catch (err) { - console.error('WebApi logout error:', err); - } - - authContext.logout(statusError); + await webApi.logout(statusError); return; } @@ -66,13 +58,7 @@ const CredentialsList: React.FC = () => { const vaultError = webApi.validateVaultResponse(vaultResponseJson); if (vaultError) { - try { - await webApi.logout(); - } catch (err) { - console.error('WebApi logout error:', err); - } - - authContext.logout(vaultError); + await webApi.logout(vaultError); hideLoading(); return; } @@ -85,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. diff --git a/browser-extensions/chrome/src/app/pages/Logout.tsx b/browser-extensions/chrome/src/app/pages/Logout.tsx index 6f36049a1..9a2c253f8 100644 --- a/browser-extensions/chrome/src/app/pages/Logout.tsx +++ b/browser-extensions/chrome/src/app/pages/Logout.tsx @@ -18,13 +18,7 @@ const Logout: React.FC = () => { * Perform logout via async method to ensure logout is completed before navigating to home page. */ const performLogout = async () : Promise => { - try { - await webApi.logout(); - } catch (err) { - console.error('WebApi logout error:', err); - } - - await authContext.logout(); + await webApi.logout(); navigate('/'); }; diff --git a/browser-extensions/chrome/src/app/pages/Unlock.tsx b/browser-extensions/chrome/src/app/pages/Unlock.tsx index 8c8c27f6b..5799fe5c4 100644 --- a/browser-extensions/chrome/src/app/pages/Unlock.tsx +++ b/browser-extensions/chrome/src/app/pages/Unlock.tsx @@ -31,13 +31,7 @@ const Unlock: React.FC = () => { const statusResponse = await webApi.getStatus(); const statusError = webApi.validateStatusResponse(statusResponse); if (statusError !== null) { - try { - await webApi.logout(); - } catch (err) { - console.error('WebApi logout error:', err); - } - - authContext.logout(statusError); + await webApi.logout(statusError); } }; diff --git a/browser-extensions/chrome/src/shared/WebApiService.ts b/browser-extensions/chrome/src/shared/WebApiService.ts index f3725c3f1..6b1f667b5 100644 --- a/browser-extensions/chrome/src/shared/WebApiService.ts +++ b/browser-extensions/chrome/src/shared/WebApiService.ts @@ -19,9 +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) { } + public constructor(private readonly authContextLogout: (statusError: string | null) => void) { } /** * Get the base URL for the API from settings. @@ -79,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'); } } @@ -128,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; } } @@ -199,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 { - const refreshToken = await this.getRefreshToken(); - if (!refreshToken) { - return; + public async logout(statusError: string | null = null): Promise { + // 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); } /**