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