mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 14:50:11 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8942e5bd49 | ||
|
|
d722815999 |
@@ -56,6 +56,7 @@ type PendingRestoreIntegrity =
|
|||||||
type PendingBackupVerification =
|
type PendingBackupVerification =
|
||||||
| { action: 'export' }
|
| { action: 'export' }
|
||||||
| { action: 'saveSettings' }
|
| { action: 'saveSettings' }
|
||||||
|
| { action: 'deleteDestination'; destinationId: string; settings: AdminBackupSettings }
|
||||||
| { action: 'import'; replaceExisting: boolean; allowChecksumMismatch: boolean; knownIntegrity?: BackupFileIntegrityCheckResult }
|
| { action: 'import'; replaceExisting: boolean; allowChecksumMismatch: boolean; knownIntegrity?: BackupFileIntegrityCheckResult }
|
||||||
| { action: 'runRemoteBackup' }
|
| { action: 'runRemoteBackup' }
|
||||||
| { action: 'downloadRemote'; path: string }
|
| { action: 'downloadRemote'; path: string }
|
||||||
@@ -240,7 +241,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
|
|||||||
const backupPasswordPromptTitle =
|
const backupPasswordPromptTitle =
|
||||||
pendingBackupVerification?.action === 'export'
|
pendingBackupVerification?.action === 'export'
|
||||||
? t('txt_backup_export')
|
? t('txt_backup_export')
|
||||||
: pendingBackupVerification?.action === 'saveSettings'
|
: pendingBackupVerification?.action === 'saveSettings' || pendingBackupVerification?.action === 'deleteDestination'
|
||||||
? t('txt_backup_save_settings')
|
? t('txt_backup_save_settings')
|
||||||
: pendingBackupVerification?.action === 'runRemoteBackup'
|
: pendingBackupVerification?.action === 'runRemoteBackup'
|
||||||
? t('txt_backup_run_manual')
|
? t('txt_backup_run_manual')
|
||||||
@@ -501,10 +502,16 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
|
|||||||
destinations: (savedSettings?.destinations || []).filter((destination) => destination.id !== destinationIdToDelete),
|
destinations: (savedSettings?.destinations || []).filter((destination) => destination.id !== destinationIdToDelete),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setPendingBackupVerification({ action: 'deleteDestination', destinationId: destinationIdToDelete, settings: nextSettings });
|
||||||
|
setBackupPasswordValue('');
|
||||||
|
setConfirmDeleteDestinationOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeDeleteDestination(masterPassword: string, destinationIdToDelete: string, payload: AdminBackupSettings) {
|
||||||
setSavingSettings(true);
|
setSavingSettings(true);
|
||||||
setLocalError('');
|
setLocalError('');
|
||||||
try {
|
try {
|
||||||
const saved = await props.onSaveSettings(nextSettings);
|
const saved = await props.onSaveSettings(masterPassword, payload);
|
||||||
const nextDraftDestinations = settings.destinations.filter((destination) => destination.id !== destinationIdToDelete);
|
const nextDraftDestinations = settings.destinations.filter((destination) => destination.id !== destinationIdToDelete);
|
||||||
const nextSelected = getFirstVisibleDestinationId({ destinations: nextDraftDestinations }) || getFirstVisibleDestinationId(saved);
|
const nextSelected = getFirstVisibleDestinationId({ destinations: nextDraftDestinations }) || getFirstVisibleDestinationId(saved);
|
||||||
setSavedSettings(saved);
|
setSavedSettings(saved);
|
||||||
@@ -865,6 +872,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
|
|||||||
await executeExport(masterPassword);
|
await executeExport(masterPassword);
|
||||||
} else if (request.action === 'saveSettings') {
|
} else if (request.action === 'saveSettings') {
|
||||||
await executeSaveSettings(masterPassword);
|
await executeSaveSettings(masterPassword);
|
||||||
|
} else if (request.action === 'deleteDestination') {
|
||||||
|
await executeDeleteDestination(masterPassword, request.destinationId, request.settings);
|
||||||
} else if (request.action === 'import') {
|
} else if (request.action === 'import') {
|
||||||
await executeLocalRestore(masterPassword, request.replaceExisting, request.allowChecksumMismatch, request.knownIntegrity);
|
await executeLocalRestore(masterPassword, request.replaceExisting, request.allowChecksumMismatch, request.knownIntegrity);
|
||||||
} else if (request.action === 'runRemoteBackup') {
|
} else if (request.action === 'runRemoteBackup') {
|
||||||
|
|||||||
@@ -234,24 +234,33 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
|||||||
const normalizedName = String(name || '').trim() || t('txt_account_passkey');
|
const normalizedName = String(name || '').trim() || t('txt_account_passkey');
|
||||||
const derived = await deriveLoginHash(profile.email, normalizedPassword, defaultKdfIterations);
|
const derived = await deriveLoginHash(profile.email, normalizedPassword, defaultKdfIterations);
|
||||||
const options = await getAccountPasskeyAttestationOptions(authedFetch, derived.hash);
|
const options = await getAccountPasskeyAttestationOptions(authedFetch, derived.hash);
|
||||||
const pending = await createAccountPasskeyCredential(options);
|
const pending = await createAccountPasskeyCredential(options, directUnlock);
|
||||||
let keySet = null;
|
let keySet = null;
|
||||||
let savedWithoutDirectUnlock = false;
|
let savedWithoutDirectUnlock = false;
|
||||||
if (directUnlock) {
|
if (directUnlock) {
|
||||||
if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable'));
|
if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable'));
|
||||||
try {
|
if (!pending.supportsPrf) {
|
||||||
keySet = await buildAccountPasskeyPrfKeySet(pending, {
|
|
||||||
symEncKey: session.symEncKey,
|
|
||||||
symMacKey: session.symMacKey,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (!(error instanceof AccountPasskeyPrfUnavailableError)) throw error;
|
|
||||||
const shouldSaveLoginOnly = await confirmSaveLoginOnlyAccountPasskey();
|
const shouldSaveLoginOnly = await confirmSaveLoginOnlyAccountPasskey();
|
||||||
if (!shouldSaveLoginOnly) {
|
if (!shouldSaveLoginOnly) {
|
||||||
onNotify('warning', t('txt_account_passkey_not_saved'));
|
onNotify('warning', t('txt_account_passkey_not_saved'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
savedWithoutDirectUnlock = true;
|
savedWithoutDirectUnlock = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
keySet = await buildAccountPasskeyPrfKeySet(pending, {
|
||||||
|
symEncKey: session.symEncKey,
|
||||||
|
symMacKey: session.symMacKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof AccountPasskeyPrfUnavailableError)) throw error;
|
||||||
|
const shouldSaveLoginOnly = await confirmSaveLoginOnlyAccountPasskey();
|
||||||
|
if (!shouldSaveLoginOnly) {
|
||||||
|
onNotify('warning', t('txt_account_passkey_not_saved'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
savedWithoutDirectUnlock = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const credential = await saveAccountPasskey(authedFetch, {
|
const credential = await saveAccountPasskey(authedFetch, {
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ function shouldRetryWithLegacyPrf(error: unknown): boolean {
|
|||||||
return name === 'NotSupportedError' || name === 'SyntaxError' || name === 'TypeError';
|
return name === 'NotSupportedError' || name === 'SyntaxError' || name === 'TypeError';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRetryCreateWithoutPrf(error: unknown): boolean {
|
||||||
|
const name = error instanceof DOMException || error instanceof Error ? error.name : '';
|
||||||
|
const message = error instanceof DOMException || error instanceof Error ? error.message : '';
|
||||||
|
return (
|
||||||
|
name === 'NotSupportedError' ||
|
||||||
|
name === 'SyntaxError' ||
|
||||||
|
name === 'TypeError' ||
|
||||||
|
(name === 'UnknownError' && /transient/i.test(message))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canRequestPrfExtension(): Promise<boolean> {
|
||||||
|
if (/\bFirefox\//i.test(navigator.userAgent)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function getPublicKeyCredentialWithPrf(
|
async function getPublicKeyCredentialWithPrf(
|
||||||
options: PublicKeyCredentialRequestOptions,
|
options: PublicKeyCredentialRequestOptions,
|
||||||
salt: Uint8Array,
|
salt: Uint8Array,
|
||||||
@@ -265,17 +281,38 @@ export async function assertAccountPasskey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createAccountPasskeyCredential(
|
export async function createAccountPasskeyCredential(
|
||||||
response: { options: unknown; token: string }
|
response: { options: unknown; token: string },
|
||||||
|
requestPrf: boolean = false
|
||||||
): Promise<PendingAccountPasskeyCredential> {
|
): Promise<PendingAccountPasskeyCredential> {
|
||||||
if (!window.PublicKeyCredential || !navigator.credentials) {
|
if (!window.PublicKeyCredential || !navigator.credentials) {
|
||||||
throw new Error(t('txt_passkey_browser_not_supported'));
|
throw new Error(t('txt_passkey_browser_not_supported'));
|
||||||
}
|
}
|
||||||
const nativeOptions = cloneCreationOptions(response.options);
|
const nativeOptions = cloneCreationOptions(response.options);
|
||||||
(nativeOptions as any).extensions = {
|
const createWithOptions = async (options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential> => {
|
||||||
...((nativeOptions as any).extensions || {}),
|
const credential = await navigator.credentials.create({ publicKey: options });
|
||||||
prf: {},
|
if (!(credential instanceof PublicKeyCredential)) {
|
||||||
|
throw new Error(t('txt_no_passkey_created'));
|
||||||
|
}
|
||||||
|
return credential;
|
||||||
};
|
};
|
||||||
const credential = await navigator.credentials.create({ publicKey: nativeOptions });
|
let credential: PublicKeyCredential;
|
||||||
|
if (requestPrf && await canRequestPrfExtension()) {
|
||||||
|
const prfOptions: PublicKeyCredentialCreationOptions = {
|
||||||
|
...nativeOptions,
|
||||||
|
extensions: {
|
||||||
|
...((nativeOptions as any).extensions || {}),
|
||||||
|
prf: {},
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
credential = await createWithOptions(prfOptions);
|
||||||
|
} catch (error) {
|
||||||
|
if (!shouldRetryCreateWithoutPrf(error)) throw error;
|
||||||
|
credential = await createWithOptions(nativeOptions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
credential = await createWithOptions(nativeOptions);
|
||||||
|
}
|
||||||
if (!(credential instanceof PublicKeyCredential)) {
|
if (!(credential instanceof PublicKeyCredential)) {
|
||||||
throw new Error(t('txt_no_passkey_created'));
|
throw new Error(t('txt_no_passkey_created'));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user