mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-15 02:45:26 -04:00
Update passkey selection UI (#520)
This commit is contained in:
@@ -3,7 +3,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
|
||||
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
|
||||
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handleStorePasskey, handleUpdatePasskeyLastUsed, handleClearAllPasskeys, handlePasskeyPopupResponse, initializePasskeys, handleGetRequestData, handleGetPasskeyById } from '@/entrypoints/background/PasskeyHandler';
|
||||
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handleStorePasskey, handleUpdatePasskeyLastUsed, handleClearAllPasskeys, handleDeletePasskey, handlePasskeyPopupResponse, initializePasskeys, handleGetRequestData, handleGetPasskeyById } from '@/entrypoints/background/PasskeyHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
@@ -70,6 +70,7 @@ export default defineBackground({
|
||||
onMessage('STORE_PASSKEY', ({ data }) => handleStorePasskey(data as { rpId: string; credentialId: string; displayName: string; publicKey: unknown }));
|
||||
onMessage('UPDATE_PASSKEY_LAST_USED', ({ data }) => handleUpdatePasskeyLastUsed(data as { credentialId: string; newSignCount?: number }));
|
||||
onMessage('CLEAR_ALL_PASSKEYS', () => handleClearAllPasskeys());
|
||||
onMessage('DELETE_PASSKEY', ({ data }) => handleDeletePasskey(data as { credentialId: string }));
|
||||
onMessage('PASSKEY_POPUP_RESPONSE', ({ data }) => handlePasskeyPopupResponse(data as { requestId: string; credential?: any; fallback?: boolean; cancelled?: boolean }));
|
||||
onMessage('GET_REQUEST_DATA', ({ data }) => handleGetRequestData(data as { requestId: string }));
|
||||
onMessage('GET_PASSKEY_BY_ID', ({ data }) => handleGetPasskeyById(data as { credentialId: string }));
|
||||
|
||||
@@ -250,7 +250,29 @@ export async function handleStorePasskey(data: {
|
||||
* In production, this would be stored in the vault database
|
||||
* For now, also store in local storage for persistence across reloads
|
||||
*/
|
||||
const storedPasskeys: Record<string, IPasskeyData> = await storage.getItem('local:passkeys') || {};
|
||||
let storedPasskeys: Record<string, IPasskeyData> = {};
|
||||
const rawData = await storage.getItem('local:passkeys');
|
||||
|
||||
// Handle migration from old array format or corrupted string data
|
||||
if (typeof rawData === 'string') {
|
||||
// Data was stored as stringified JSON (old format), clear it
|
||||
console.warn('PasskeyHandler: Found old/corrupted passkey storage format, migrating...');
|
||||
try {
|
||||
const parsed = JSON.parse(rawData);
|
||||
if (Array.isArray(parsed)) {
|
||||
// Convert array to record format
|
||||
for (const pk of parsed) {
|
||||
const pkKey = `${pk.rpId}:${pk.credentialId}`;
|
||||
storedPasskeys[pkKey] = pk;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('PasskeyHandler: Failed to migrate old passkey data', e);
|
||||
}
|
||||
} else if (rawData && typeof rawData === 'object') {
|
||||
storedPasskeys = rawData as Record<string, IPasskeyData>;
|
||||
}
|
||||
|
||||
storedPasskeys[key] = passkey;
|
||||
await storage.setItem('local:passkeys', storedPasskeys);
|
||||
|
||||
@@ -275,7 +297,13 @@ export async function handleUpdatePasskeyLastUsed(data: {
|
||||
sessionPasskeys.set(key, passkey);
|
||||
|
||||
// Update in storage too
|
||||
const storedPasskeys: Record<string, IPasskeyData> = await storage.getItem('local:passkeys') || {};
|
||||
const rawData = await storage.getItem('local:passkeys');
|
||||
let storedPasskeys: Record<string, IPasskeyData> = {};
|
||||
|
||||
if (typeof rawData === 'object' && rawData !== null) {
|
||||
storedPasskeys = rawData as Record<string, IPasskeyData>;
|
||||
}
|
||||
|
||||
if (storedPasskeys[key]) {
|
||||
storedPasskeys[key] = passkey;
|
||||
await storage.setItem('local:passkeys', storedPasskeys);
|
||||
@@ -310,7 +338,29 @@ function getPasskeysForOrigin(origin: string): IPasskeyData[] {
|
||||
* Initialize passkeys from storage on startup
|
||||
*/
|
||||
export async function initializePasskeys(): Promise<void> {
|
||||
const storedPasskeys: Record<string, IPasskeyData> = await storage.getItem('local:passkeys') || {};
|
||||
const rawData = await storage.getItem('local:passkeys');
|
||||
let storedPasskeys: Record<string, IPasskeyData> = {};
|
||||
|
||||
// Handle migration from old array format or corrupted string data
|
||||
if (typeof rawData === 'string') {
|
||||
console.warn('PasskeyHandler: Found old/corrupted passkey storage format during init, migrating...');
|
||||
try {
|
||||
const parsed = JSON.parse(rawData);
|
||||
if (Array.isArray(parsed)) {
|
||||
// Convert array to record format
|
||||
for (const pk of parsed) {
|
||||
const pkKey = `${pk.rpId}:${pk.credentialId}`;
|
||||
storedPasskeys[pkKey] = pk;
|
||||
}
|
||||
// Save migrated data
|
||||
await storage.setItem('local:passkeys', storedPasskeys);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('PasskeyHandler: Failed to migrate old passkey data during init', e);
|
||||
}
|
||||
} else if (rawData && typeof rawData === 'object') {
|
||||
storedPasskeys = rawData as Record<string, IPasskeyData>;
|
||||
}
|
||||
|
||||
for (const [key, passkey] of Object.entries(storedPasskeys)) {
|
||||
sessionPasskeys.set(key, passkey as IPasskeyData);
|
||||
@@ -385,3 +435,29 @@ export async function handleClearAllPasskeys(): Promise<{ success: boolean }> {
|
||||
await storage.removeItem('local:passkeys');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific passkey
|
||||
*/
|
||||
export async function handleDeletePasskey(data: { credentialId: string }): Promise<{ success: boolean }> {
|
||||
const { credentialId } = data;
|
||||
|
||||
// Find and remove from session storage
|
||||
let deletedKey: string | null = null;
|
||||
for (const [key, passkey] of sessionPasskeys.entries()) {
|
||||
if (passkey.credentialId === credentialId) {
|
||||
sessionPasskeys.delete(key);
|
||||
deletedKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedKey) {
|
||||
// Remove from storage (storage API expects Record format, not stringified)
|
||||
const storedPasskeys: Record<string, IPasskeyData> = await storage.getItem('local:passkeys') || {};
|
||||
delete storedPasskeys[deletedKey];
|
||||
await storage.setItem('local:passkeys', storedPasskeys);
|
||||
}
|
||||
|
||||
return { success: !!deletedKey };
|
||||
}
|
||||
|
||||
@@ -74,10 +74,10 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
}, [location, setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
*
|
||||
* Handle passkey authentication
|
||||
*/
|
||||
const handleUsePasskey = async () => {
|
||||
if (!request || !selectedPasskey) {
|
||||
const handleUsePasskey = async (credentialId: string) => {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
|
||||
try {
|
||||
console.log('PasskeyAuthenticate: Starting authentication');
|
||||
console.log('PasskeyAuthenticate: selectedPasskey', selectedPasskey);
|
||||
console.log('PasskeyAuthenticate: credentialId', credentialId);
|
||||
console.log('PasskeyAuthenticate: request', request);
|
||||
|
||||
// Create the provider with storage callbacks
|
||||
@@ -112,16 +112,16 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
};
|
||||
|
||||
// Get the assertion using the provider
|
||||
const credential = await provider.getAssertion(getRequest, selectedPasskey, {
|
||||
uvPerformed: false, // Set to true if you implement actual user verification
|
||||
const credential = await provider.getAssertion(getRequest, credentialId, {
|
||||
uvPerformed: true, // Set to true if you implement actual user verification
|
||||
includeBEBS: true // Include backup-eligible and backup-state flags
|
||||
});
|
||||
|
||||
console.info('PasskeyAuthenticate: Created credential successfully', credential);
|
||||
console.info('PasskeyAuthenticate: Received assertion successfully', credential);
|
||||
|
||||
// Update last used timestamp
|
||||
await sendMessage('UPDATE_PASSKEY_LAST_USED', {
|
||||
credentialId: selectedPasskey
|
||||
credentialId
|
||||
}, 'background');
|
||||
|
||||
// Send response back
|
||||
@@ -138,6 +138,35 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle passkey deletion
|
||||
*/
|
||||
const handleDeletePasskey = async (credentialId: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation(); // Prevent triggering authentication
|
||||
|
||||
if (!confirm('Are you sure you want to delete this passkey?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendMessage('DELETE_PASSKEY', { credentialId }, 'background');
|
||||
|
||||
// Update the request to remove the deleted passkey
|
||||
if (request?.passkeys) {
|
||||
const updatedPasskeys = request.passkeys.filter(pk => pk.id !== credentialId);
|
||||
setRequest({ ...request, passkeys: updatedPasskeys });
|
||||
|
||||
// Clear selection if the deleted passkey was selected
|
||||
if (selectedPasskey === credentialId) {
|
||||
setSelectedPasskey(null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete passkey:', error);
|
||||
alert(`Failed to delete passkey: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -195,24 +224,34 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
{request.passkeys && request.passkeys.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Select a passkey:
|
||||
Select a passkey to sign in:
|
||||
</label>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto border rounded-lg p-2 bg-gray-50 dark:bg-gray-800">
|
||||
{request.passkeys.map((pk) => (
|
||||
<div
|
||||
key={pk.id}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedPasskey === pk.id
|
||||
? 'bg-blue-50 border-blue-200 dark:bg-blue-900 dark:border-blue-700'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
onClick={() => setSelectedPasskey(pk.id)}
|
||||
className="relative group p-3 rounded-lg border cursor-pointer transition-colors bg-white border-gray-200 hover:bg-blue-50 hover:border-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-blue-900 dark:hover:border-blue-700"
|
||||
onClick={() => !loading && handleUsePasskey(pk.id)}
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-white text-sm">
|
||||
{pk.displayName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Last used: {pk.lastUsed || 'Never'}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 dark:text-white text-sm truncate">
|
||||
{pk.displayName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Last used: {pk.lastUsed || 'Never'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDeletePasskey(pk.id, e)}
|
||||
className="flex-shrink-0 p-1.5 text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="Delete passkey"
|
||||
disabled={loading}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -228,17 +267,6 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{request.passkeys && request.passkeys.length > 0 && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleUsePasskey}
|
||||
disabled={loading || !selectedPasskey}
|
||||
className="w-full"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Use Selected Passkey'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleFallback}
|
||||
|
||||
Reference in New Issue
Block a user