mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Refactor all vault mutate calls to use async method (#1404)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -8,18 +7,13 @@ import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useVaultSync } from './useVaultSync';
|
||||
|
||||
type VaultMutationOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to execute a vault mutation.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Execute the mutation on local database
|
||||
* 2. Save encrypted vault locally and mark as dirty (increments mutation sequence)
|
||||
* 3. Trigger sync which handles: upload, merge if needed, offline mode
|
||||
* 3. Trigger sync in background which handles: upload, merge if needed, offline mode
|
||||
*
|
||||
* The mutation sequence is used for race detection:
|
||||
* - Each mutation increments the sequence
|
||||
@@ -27,14 +21,8 @@ type VaultMutationOptions = {
|
||||
* - This ensures we never lose local changes during concurrent operations
|
||||
*/
|
||||
export function useVaultMutate(): {
|
||||
executeVaultMutation: (operation: () => Promise<void>, options?: VaultMutationOptions) => Promise<void>;
|
||||
executeVaultMutationAsync: (operation: () => Promise<void>) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
syncStatus: string;
|
||||
} {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState('');
|
||||
const dbContext = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
@@ -131,67 +119,7 @@ export function useVaultMutate(): {
|
||||
void triggerSync();
|
||||
}, [saveLocally, triggerSync]);
|
||||
|
||||
/**
|
||||
* Execute a vault mutation: save locally, then sync with server.
|
||||
*
|
||||
* The sync handles all scenarios:
|
||||
* - Online + same revision + isDirty → upload
|
||||
* - Online + server newer + isDirty → merge + upload
|
||||
* - Offline → changes are safe locally, will sync when back online
|
||||
*/
|
||||
const executeVaultMutation = useCallback(async (
|
||||
operation: () => Promise<void>,
|
||||
options: VaultMutationOptions = {}
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSyncStatus(t('common.savingChangesToVault'));
|
||||
|
||||
// 1. Execute mutation and save locally (always succeeds if no exceptions)
|
||||
await saveLocally(operation);
|
||||
|
||||
// 2. Sync with server (handles upload, merge, offline - everything)
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle status updates during sync.
|
||||
* @param message - Status message to display
|
||||
*/
|
||||
onStatus: (message) => setSyncStatus(message),
|
||||
/**
|
||||
* Handle successful sync completion.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Refresh state from storage
|
||||
await dbContext.refreshSyncState();
|
||||
options.onSuccess?.();
|
||||
},
|
||||
/**
|
||||
* Handle offline mode - local save succeeded.
|
||||
*/
|
||||
onOffline: () => {
|
||||
// Local save succeeded, user can continue working offline
|
||||
setSyncStatus(t('common.offlineModeSaved'));
|
||||
options.onSuccess?.();
|
||||
},
|
||||
/**
|
||||
* Handle sync errors.
|
||||
* @param error - Error message from sync
|
||||
*/
|
||||
onError: (error) => options.onError?.(new Error(error))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during vault mutation:', error);
|
||||
options.onError?.(error instanceof Error ? error : new Error(t('common.errors.unknownError')));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setSyncStatus('');
|
||||
}
|
||||
}, [dbContext, saveLocally, syncVault, t]);
|
||||
|
||||
return {
|
||||
executeVaultMutation,
|
||||
executeVaultMutationAsync,
|
||||
isLoading,
|
||||
syncStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const Upgrade: React.FC = () => {
|
||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -127,7 +127,7 @@ const Upgrade: React.FC = () => {
|
||||
}
|
||||
|
||||
// Use the useVaultMutate hook to handle the upgrade and vault upload
|
||||
await executeVaultMutation(async () => {
|
||||
await executeVaultMutationAsync(async () => {
|
||||
// Begin transaction
|
||||
sqliteClient.beginTransaction();
|
||||
|
||||
@@ -146,21 +146,9 @@ const Upgrade: React.FC = () => {
|
||||
|
||||
// Commit transaction
|
||||
sqliteClient.commitTransaction();
|
||||
}, {
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void handleUpgradeSuccess();
|
||||
},
|
||||
/**
|
||||
* Handle upgrade error.
|
||||
*/
|
||||
onError: (error: Error) => {
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
await handleUpgradeSuccess();
|
||||
} catch (error) {
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error instanceof Error ? error.message : t('common.errors.unknownError'));
|
||||
@@ -217,11 +205,11 @@ const Upgrade: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Full loading screen overlay */}
|
||||
{(isLoading || isVaultMutationLoading) && (
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus || t('upgrade.upgrading')}
|
||||
{t('upgrade.upgrading')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -313,13 +301,13 @@ const Upgrade: React.FC = () => {
|
||||
id="upgrade-button"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || t('upgrade.upgrading')) : t('upgrade.upgrade')}
|
||||
{isLoading ? t('upgrade.upgrading') : t('upgrade.upgrade')}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium py-2"
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('upgrade.logout')}
|
||||
</button>
|
||||
|
||||
@@ -91,7 +91,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
Notes: Yup.string().nullable().optional()
|
||||
}), [t]);
|
||||
|
||||
const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
@@ -381,18 +381,13 @@ const CredentialAddEdit: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
await executeVaultMutationAsync(async () => {
|
||||
dbContext.sqliteClient!.deleteCredentialById(id);
|
||||
}, {
|
||||
/**
|
||||
* Navigate to the credentials list page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate, clearPersistedValues]);
|
||||
|
||||
void clearPersistedValues();
|
||||
navigate('/credentials');
|
||||
}, [id, executeVaultMutationAsync, dbContext.sqliteClient, navigate, clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
@@ -595,7 +590,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
await executeVaultMutationAsync(async () => {
|
||||
setLocalLoading(false);
|
||||
|
||||
if (isEditMode) {
|
||||
@@ -609,23 +604,18 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments, totpCodes);
|
||||
data.Id = credentialId.toString();
|
||||
}
|
||||
}, {
|
||||
/**
|
||||
* Navigate to the credential details page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
// If in add mode, navigate to the credential details page.
|
||||
if (!isEditMode) {
|
||||
// Navigate to the credential details page.
|
||||
navigate(`/credentials/${data.Id}`, { replace: true });
|
||||
} else {
|
||||
// If in edit mode, pop the current page from the history stack to end up on details page as well.
|
||||
navigate(-1);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyMarkedForDeletion]);
|
||||
|
||||
void clearPersistedValues();
|
||||
// If in add mode, navigate to the credential details page.
|
||||
if (!isEditMode) {
|
||||
// Navigate to the credential details page.
|
||||
navigate(`/credentials/${data.Id}`, { replace: true });
|
||||
} else {
|
||||
// If in edit mode, pop the current page from the history stack to end up on details page as well.
|
||||
navigate(-1);
|
||||
}
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutationAsync, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyMarkedForDeletion]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -664,12 +654,9 @@ const CredentialAddEdit: React.FC = () => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit" style={{ display: 'none' }} />
|
||||
{(localLoading || isLoading) && (
|
||||
{localLoading && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ const ItemsList: React.FC = () => {
|
||||
const app = useApp();
|
||||
const navigate = useNavigate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -136,28 +136,14 @@ const ItemsList: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultMutation(
|
||||
async () => {
|
||||
await dbContext.sqliteClient!.createFolder(folderName, currentFolderId);
|
||||
},
|
||||
{
|
||||
/**
|
||||
* On success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Refresh items to show the new folder
|
||||
const results = dbContext.sqliteClient!.getAllItems();
|
||||
setItems(results);
|
||||
},
|
||||
/**
|
||||
* On error.
|
||||
*/
|
||||
onError: (error) => {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [dbContext, currentFolderId, executeVaultMutation]);
|
||||
await executeVaultMutationAsync(async () => {
|
||||
await dbContext.sqliteClient!.createFolder(folderName, currentFolderId);
|
||||
});
|
||||
|
||||
// Refresh items to show the new folder
|
||||
const results = dbContext.sqliteClient!.getAllItems();
|
||||
setItems(results);
|
||||
}, [dbContext, currentFolderId, executeVaultMutationAsync]);
|
||||
|
||||
/**
|
||||
* Retrieve latest vault and refresh the items list.
|
||||
|
||||
@@ -30,7 +30,7 @@ const getDaysRemaining = (deletedAt: string, retentionDays: number = 30): number
|
||||
const RecentlyDeleted: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
|
||||
@@ -60,27 +60,12 @@ const RecentlyDeleted: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultMutation(
|
||||
async () => {
|
||||
await dbContext.sqliteClient!.restoreItem(itemId);
|
||||
},
|
||||
{
|
||||
/**
|
||||
* On success callback.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
loadItems();
|
||||
},
|
||||
/**
|
||||
* On error callback.
|
||||
* @param error - The error that occurred
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Error restoring item:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutation, loadItems]);
|
||||
await executeVaultMutationAsync(async () => {
|
||||
await dbContext.sqliteClient!.restoreItem(itemId);
|
||||
});
|
||||
|
||||
loadItems();
|
||||
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems]);
|
||||
|
||||
/**
|
||||
* Permanently delete an item.
|
||||
@@ -90,29 +75,14 @@ const RecentlyDeleted: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultMutation(
|
||||
async () => {
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(itemId);
|
||||
},
|
||||
{
|
||||
/**
|
||||
* On success callback.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
loadItems();
|
||||
setShowConfirmDelete(false);
|
||||
setSelectedItemId(null);
|
||||
},
|
||||
/**
|
||||
* On error callback.
|
||||
* @param error - The error that occurred
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Error permanently deleting item:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutation, loadItems]);
|
||||
await executeVaultMutationAsync(async () => {
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(itemId);
|
||||
});
|
||||
|
||||
loadItems();
|
||||
setShowConfirmDelete(false);
|
||||
setSelectedItemId(null);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems]);
|
||||
|
||||
/**
|
||||
* Empty all items from Recently Deleted (permanent delete all).
|
||||
@@ -122,30 +92,15 @@ const RecentlyDeleted: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultMutation(
|
||||
async () => {
|
||||
for (const item of items) {
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(item.Id);
|
||||
}
|
||||
},
|
||||
{
|
||||
/**
|
||||
* On success callback.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
loadItems();
|
||||
setShowConfirmEmptyAll(false);
|
||||
},
|
||||
/**
|
||||
* On error callback.
|
||||
* @param error - The error that occurred
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Error emptying recently deleted:', error);
|
||||
}
|
||||
await executeVaultMutationAsync(async () => {
|
||||
for (const item of items) {
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(item.Id);
|
||||
}
|
||||
);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutation, items, loadItems]);
|
||||
});
|
||||
|
||||
loadItems();
|
||||
setShowConfirmEmptyAll(false);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutationAsync, items, loadItems]);
|
||||
|
||||
// Clear header buttons on mount
|
||||
useEffect((): (() => void) => {
|
||||
|
||||
@@ -32,7 +32,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isMutating, syncStatus } = useVaultMutate();
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const [request, setRequest] = useState<PendingPasskeyCreateRequest | null>(null);
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -156,7 +156,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
* Handle Enter key to submit
|
||||
*/
|
||||
const handleKeyDown = (e: KeyboardEvent) : void => {
|
||||
if (e.key === 'Enter' && !localLoading && !isMutating) {
|
||||
if (e.key === 'Enter' && !localLoading) {
|
||||
if (showCreateForm) {
|
||||
handleCreate();
|
||||
}
|
||||
@@ -166,7 +166,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () : void => window.removeEventListener('keydown', handleKeyDown);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showCreateForm, localLoading, isMutating]);
|
||||
}, [showCreateForm, localLoading]);
|
||||
|
||||
/**
|
||||
* Handle when user clicks "Create New Passkey" button
|
||||
@@ -259,69 +259,15 @@ const PasskeyCreate: React.FC = () => {
|
||||
const { credential, stored, prfEnabled, prfResults } = result;
|
||||
|
||||
// Use vault mutation to store both credential and passkey
|
||||
await executeVaultMutation(
|
||||
async () => {
|
||||
if (selectedPasskeyToReplace) {
|
||||
// Replace existing passkey: update the credential and passkey
|
||||
const existingPasskey = dbContext.sqliteClient!.getPasskeyById(selectedPasskeyToReplace);
|
||||
if (existingPasskey) {
|
||||
// Update the parent credential with new favicon and user-provided display name
|
||||
await dbContext.sqliteClient!.updateCredentialById(
|
||||
{
|
||||
Id: existingPasskey.CredentialId,
|
||||
ServiceName: displayName,
|
||||
ServiceUrl: request.origin,
|
||||
Username: request.publicKey.user.name,
|
||||
Password: '',
|
||||
Notes: '',
|
||||
Logo: faviconLogo ?? undefined,
|
||||
Alias: {
|
||||
FirstName: '',
|
||||
LastName: '',
|
||||
NickName: '',
|
||||
BirthDate: '0001-01-01 00:00:00',
|
||||
Gender: '',
|
||||
Email: ''
|
||||
},
|
||||
},
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
// Delete the old passkey
|
||||
await dbContext.sqliteClient!.deletePasskeyById(selectedPasskeyToReplace);
|
||||
|
||||
/**
|
||||
* Create new passkey with same credential
|
||||
* Convert userId from base64 string to byte array for database storage
|
||||
*/
|
||||
let userHandleBytes: Uint8Array | null = null;
|
||||
if (stored.userId) {
|
||||
try {
|
||||
userHandleBytes = PasskeyHelper.base64urlToBytes(stored.userId);
|
||||
} catch {
|
||||
// If conversion fails, store as null
|
||||
userHandleBytes = null;
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.sqliteClient!.createPasskey({
|
||||
Id: newPasskeyGuid,
|
||||
CredentialId: existingPasskey.CredentialId,
|
||||
RpId: stored.rpId,
|
||||
UserHandle: userHandleBytes,
|
||||
PublicKey: JSON.stringify(stored.publicKey),
|
||||
PrivateKey: JSON.stringify(stored.privateKey),
|
||||
DisplayName: request.publicKey.user.displayName || request.publicKey.user.name || '',
|
||||
PrfKey: stored.prfSecret ? PasskeyHelper.base64urlToBytes(stored.prfSecret) : undefined,
|
||||
AdditionalData: null
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create new credential and passkey
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(
|
||||
await executeVaultMutationAsync(async () => {
|
||||
if (selectedPasskeyToReplace) {
|
||||
// Replace existing passkey: update the credential and passkey
|
||||
const existingPasskey = dbContext.sqliteClient!.getPasskeyById(selectedPasskeyToReplace);
|
||||
if (existingPasskey) {
|
||||
// Update the parent credential with new favicon and user-provided display name
|
||||
await dbContext.sqliteClient!.updateCredentialById(
|
||||
{
|
||||
Id: '',
|
||||
Id: existingPasskey.CredentialId,
|
||||
ServiceName: displayName,
|
||||
ServiceUrl: request.origin,
|
||||
Username: request.publicKey.user.name,
|
||||
@@ -332,17 +278,20 @@ const PasskeyCreate: React.FC = () => {
|
||||
FirstName: '',
|
||||
LastName: '',
|
||||
NickName: '',
|
||||
BirthDate: '0001-01-01 00:00:00', // TODO: once birthdate is made nullable in datamodel refactor, remove this.
|
||||
BirthDate: '0001-01-01 00:00:00',
|
||||
Gender: '',
|
||||
Email: ''
|
||||
}
|
||||
},
|
||||
},
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
// Delete the old passkey
|
||||
await dbContext.sqliteClient!.deletePasskeyById(selectedPasskeyToReplace);
|
||||
|
||||
/**
|
||||
* Create the Passkey linked to the credential
|
||||
* Note: We let the database generate a GUID for Id, which we'll convert to base64url for the RP
|
||||
* Create new passkey with same credential
|
||||
* Convert userId from base64 string to byte array for database storage
|
||||
*/
|
||||
let userHandleBytes: Uint8Array | null = null;
|
||||
@@ -357,7 +306,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
|
||||
await dbContext.sqliteClient!.createPasskey({
|
||||
Id: newPasskeyGuid,
|
||||
CredentialId: credentialId,
|
||||
CredentialId: existingPasskey.CredentialId,
|
||||
RpId: stored.rpId,
|
||||
UserHandle: userHandleBytes,
|
||||
PublicKey: JSON.stringify(stored.publicKey),
|
||||
@@ -367,54 +316,91 @@ const PasskeyCreate: React.FC = () => {
|
||||
AdditionalData: null
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
} else {
|
||||
// Create new credential and passkey
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(
|
||||
{
|
||||
Id: '',
|
||||
ServiceName: displayName,
|
||||
ServiceUrl: request.origin,
|
||||
Username: request.publicKey.user.name,
|
||||
Password: '',
|
||||
Notes: '',
|
||||
Logo: faviconLogo ?? undefined,
|
||||
Alias: {
|
||||
FirstName: '',
|
||||
LastName: '',
|
||||
NickName: '',
|
||||
BirthDate: '0001-01-01 00:00:00', // TODO: once birthdate is made nullable in datamodel refactor, remove this.
|
||||
Gender: '',
|
||||
Email: ''
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Wait for vault mutation to have synced with server, then send passkey create success response
|
||||
* with the GUID-based credential ID.
|
||||
* Create the Passkey linked to the credential
|
||||
* Note: We let the database generate a GUID for Id, which we'll convert to base64url for the RP
|
||||
* Convert userId from base64 string to byte array for database storage
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Prepare PRF extension response if PRF was enabled
|
||||
let prfExtensionResponse;
|
||||
if (prfEnabled) {
|
||||
prfExtensionResponse = {
|
||||
prf: {
|
||||
enabled: true,
|
||||
results: prfResults ? {
|
||||
first: PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.first)),
|
||||
second: prfResults.second ? PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.second)) : undefined
|
||||
} : undefined
|
||||
}
|
||||
};
|
||||
let userHandleBytes: Uint8Array | null = null;
|
||||
if (stored.userId) {
|
||||
try {
|
||||
userHandleBytes = PasskeyHelper.base64urlToBytes(stored.userId);
|
||||
} catch {
|
||||
// If conversion fails, store as null
|
||||
userHandleBytes = null;
|
||||
}
|
||||
|
||||
// Use the GUID-based credential ID instead of the random one from the provider
|
||||
const flattenedCredential: PasskeyCreateCredentialResponse = {
|
||||
id: newPasskeyGuidBase64url,
|
||||
rawId: newPasskeyGuidBase64url,
|
||||
clientDataJSON: credential.response.clientDataJSON,
|
||||
attestationObject: credential.response.attestationObject,
|
||||
extensions: prfExtensionResponse
|
||||
};
|
||||
|
||||
/*
|
||||
* Send response back to background
|
||||
* The background script will close the window (Safari-compatible)
|
||||
*/
|
||||
await sendMessage('PASSKEY_POPUP_RESPONSE', {
|
||||
requestId: request.requestId,
|
||||
credential: flattenedCredential
|
||||
}, 'background');
|
||||
},
|
||||
/**
|
||||
* onError
|
||||
*/
|
||||
onError: (err) => {
|
||||
console.error('PasskeyCreate: Error storing passkey', err);
|
||||
setError(t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
await dbContext.sqliteClient!.createPasskey({
|
||||
Id: newPasskeyGuid,
|
||||
CredentialId: credentialId,
|
||||
RpId: stored.rpId,
|
||||
UserHandle: userHandleBytes,
|
||||
PublicKey: JSON.stringify(stored.publicKey),
|
||||
PrivateKey: JSON.stringify(stored.privateKey),
|
||||
DisplayName: request.publicKey.user.displayName || request.publicKey.user.name || '',
|
||||
PrfKey: stored.prfSecret ? PasskeyHelper.base64urlToBytes(stored.prfSecret) : undefined,
|
||||
AdditionalData: null
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Prepare PRF extension response if PRF was enabled
|
||||
let prfExtensionResponse;
|
||||
if (prfEnabled) {
|
||||
prfExtensionResponse = {
|
||||
prf: {
|
||||
enabled: true,
|
||||
results: prfResults ? {
|
||||
first: PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.first)),
|
||||
second: prfResults.second ? PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.second)) : undefined
|
||||
} : undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Use the GUID-based credential ID instead of the random one from the provider
|
||||
const flattenedCredential: PasskeyCreateCredentialResponse = {
|
||||
id: newPasskeyGuidBase64url,
|
||||
rawId: newPasskeyGuidBase64url,
|
||||
clientDataJSON: credential.response.clientDataJSON,
|
||||
attestationObject: credential.response.attestationObject,
|
||||
extensions: prfExtensionResponse
|
||||
};
|
||||
|
||||
/*
|
||||
* Send response back to background
|
||||
* The background script will close the window (Safari-compatible)
|
||||
*/
|
||||
await sendMessage('PASSKEY_POPUP_RESPONSE', {
|
||||
requestId: request.requestId,
|
||||
credential: flattenedCredential
|
||||
}, 'background');
|
||||
|
||||
setLocalLoading(false);
|
||||
} catch (error) {
|
||||
console.error('PasskeyCreate: Error creating passkey', error);
|
||||
setError(t('common.errors.unknownError'));
|
||||
@@ -495,12 +481,9 @@ const PasskeyCreate: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(localLoading || isMutating) && (
|
||||
{localLoading && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user