From 1e34a96c578612ae4d482fd46279e0958c5fddb9 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 5 Mar 2026 02:55:59 +0800 Subject: [PATCH] feat: improve error handling and localization for vault operations and import/export processes --- webapp/src/App.tsx | 49 +++++++++++++++------------- webapp/src/components/ImportPage.tsx | 14 ++++---- webapp/src/components/VaultPage.tsx | 6 ++-- webapp/src/lib/i18n.ts | 36 +++++++++++++++++++- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index cc15ad2..3e59733 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -110,24 +110,27 @@ function summarizeImportResult( ciphers: Array>, folderCount: number ): ImportResultSummary { - const counter = new Map(); const typeLabel = (type: number): string => { - if (type === 1) return '登录'; - if (type === 2) return '安全备注'; - if (type === 3) return '卡片'; - if (type === 4) return '身份'; - if (type === 5) return 'SSH 密钥'; - return '其他'; + if (type === 1) return t('txt_login'); + if (type === 2) return t('txt_secure_note'); + if (type === 3) return t('txt_card'); + if (type === 4) return t('txt_identity'); + if (type === 5) return t('txt_ssh_key'); + return t('txt_other'); }; + const counter = new Map(); for (const raw of ciphers) { - const t = Number(raw?.type || 1) || 1; - const label = typeLabel(t); - counter.set(label, (counter.get(label) || 0) + 1); + const cipherType = Number(raw?.type || 1) || 1; + counter.set(cipherType, (counter.get(cipherType) || 0) + 1); } - const order = ['登录', '安全备注', '卡片', '身份', 'SSH 密钥', '其他']; + const order = [1, 2, 3, 4, 5]; + const seen = new Set(order); const typeCounts = order - .filter((label) => (counter.get(label) || 0) > 0) - .map((label) => ({ label, count: counter.get(label) || 0 })); + .filter((type) => (counter.get(type) || 0) > 0) + .map((type) => ({ label: typeLabel(type), count: counter.get(type) || 0 })); + for (const [type, count] of counter.entries()) { + if (!seen.has(type) && count > 0) typeCounts.push({ label: typeLabel(type), count }); + } return { totalItems: ciphers.length, folderCount: Math.max(0, folderCount), @@ -1075,7 +1078,7 @@ export default function App() { return; } try { - if (!session) throw new Error('Vault key unavailable'); + if (!session) throw new Error(t('txt_vault_key_unavailable')); await createFolder(authedFetch, session, folderName); await foldersQuery.refetch(); pushToast('success', t('txt_folder_created')); @@ -1088,15 +1091,15 @@ export default function App() { async function deleteFolderAction(folderId: string) { const id = String(folderId || '').trim(); if (!id) { - pushToast('error', 'Folder not found'); + pushToast('error', t('txt_folder_not_found')); return; } try { await deleteFolder(authedFetch, id); await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); - pushToast('success', 'Folder deleted'); + pushToast('success', t('txt_folder_deleted')); } catch (error) { - pushToast('error', error instanceof Error ? error.message : 'Delete folder failed'); + pushToast('error', error instanceof Error ? error.message : t('txt_delete_folder_failed')); throw error; } } @@ -1120,7 +1123,7 @@ export default function App() { idMaps: { byIndex: Map; bySourceId: Map } ): Promise { if (!attachments.length) return; - if (!session?.symEncKey || !session?.symMacKey) throw new Error('Vault key unavailable'); + if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable')); const initialCiphers = (await ciphersQuery.refetch()).data || []; const cipherById = new Map(initialCiphers.map((cipher) => [String(cipher.id || ''), cipher])); @@ -1145,7 +1148,7 @@ export default function App() { } if (unresolved.length) { - throw new Error(`Failed to map ${unresolved.length} attachment(s) to imported items.`); + throw new Error(t('txt_failed_to_map_attachments', { count: unresolved.length })); } await ciphersQuery.refetch(); @@ -1172,7 +1175,7 @@ export default function App() { options: { folderMode: 'original' | 'none' | 'target'; targetFolderId: string | null }, attachments: ImportAttachmentFile[] = [] ): Promise { - if (!session?.symEncKey || !session?.symMacKey) throw new Error('Vault key unavailable'); + if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable')); const mode = options.folderMode || 'original'; const targetFolderId = (options.targetFolderId || '').trim() || null; @@ -1284,7 +1287,7 @@ export default function App() { } async function handleExportAction(request: ExportRequest) { - if (!session?.symEncKey || !session?.symMacKey) throw new Error('Vault key unavailable'); + if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable')); const masterPassword = String(request.masterPassword || '').trim(); if (!masterPassword) throw new Error(t('txt_master_password_is_required')); const email = String(profile?.email || session.email || '').trim().toLowerCase(); @@ -1294,7 +1297,7 @@ export default function App() { const rawFolders = foldersQuery.data || []; const rawCiphers = ciphersQuery.data || []; - if (!rawFolders || !rawCiphers) throw new Error('Vault is not ready yet'); + if (!rawFolders || !rawCiphers) throw new Error(t('txt_vault_not_ready')); let plainJsonCache: string | null = null; let plainJsonDocCache: Record | null = null; @@ -1512,7 +1515,7 @@ export default function App() { }; } - throw new Error('Unsupported export format'); + throw new Error(t('txt_unsupported_export_format')); } const hashPathRaw = typeof window !== 'undefined' ? window.location.hash || '' : ''; diff --git a/webapp/src/components/ImportPage.tsx b/webapp/src/components/ImportPage.tsx index df335e2..91c3220 100644 --- a/webapp/src/components/ImportPage.tsx +++ b/webapp/src/components/ImportPage.tsx @@ -118,7 +118,7 @@ async function derivePasswordProtectedFileKey( const memoryMiB = Number(parsed.kdfMemory || 0); const parallelism = Number(parsed.kdfParallelism || 0); if (!Number.isFinite(memoryMiB) || memoryMiB <= 0 || !Number.isFinite(parallelism) || parallelism <= 0) { - throw new Error('Invalid Argon2id parameters in export file.'); + throw new Error(t('txt_invalid_argon2id_params')); } const memoryKiB = Math.floor(memoryMiB * 1024); const maxmem = memoryKiB * 1024 + 1024 * 1024; @@ -131,7 +131,7 @@ async function derivePasswordProtectedFileKey( asyncTick: 10, }); } else { - throw new Error(`Unsupported kdfType: ${kdfType}`); + throw new Error(t('txt_unsupported_kdf_type', { type: String(kdfType) })); } const enc = await hkdfExpand(keyMaterial, 'enc', 32); @@ -152,7 +152,7 @@ async function decryptPasswordProtectedExport(parsed: BitwardenPasswordProtected try { await decryptStr(parsed.encKeyValidation_DO_NOT_EDIT, key.enc, key.mac); } catch { - throw new Error('Invalid file password.'); + throw new Error(t('txt_invalid_file_password')); } const plainJson = await decryptStr(parsed.data, key.enc, key.mac); @@ -317,16 +317,16 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys if (isRecord(parsed) && parsed.encrypted === true) { const accountEncrypted = parsed as BitwardenJsonInput; if (!accountKeys?.encB64 || !accountKeys?.macB64) { - throw new Error('Vault key unavailable. Please unlock vault and try again.'); + throw new Error(t('txt_vault_key_unavailable')); } const validation = String(accountEncrypted.encKeyValidation_DO_NOT_EDIT || '').trim(); - if (!validation) throw new Error('Invalid encrypted export file.'); + if (!validation) throw new Error(t('txt_invalid_encrypted_export')); const accountEncKey = base64ToBytes(accountKeys.encB64); const accountMacKey = base64ToBytes(accountKeys.macB64); try { await decryptStr(validation, accountEncKey, accountMacKey); } catch { - throw new Error('This encrypted export belongs to another account.'); + throw new Error(t('txt_export_belongs_to_another_account')); } return onImportEncryptedRaw( normalizeBitwardenEncryptedAccountImport(accountEncrypted), @@ -355,7 +355,7 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys const encryptedPayload = String(parsed.nodewardenAttachmentsEnc || '').trim(); if (!encryptedPayload) return []; if (!accountKeys?.encB64 || !accountKeys?.macB64) { - throw new Error('Vault key unavailable. Please unlock vault and try again.'); + throw new Error(t('txt_vault_key_unavailable')); } const accountEnc = base64ToBytes(accountKeys.encB64); const accountMac = base64ToBytes(accountKeys.macB64); diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index 95087cc..23a966e 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -963,7 +963,7 @@ function folderName(id: string | null | undefined): string { <>
-

{isCreating ? `New ${cipherTypeLabel(draft.type)}` : `Edit ${cipherTypeLabel(draft.type)}`}

+

{isCreating ? t('txt_new_type_header', { type: cipherTypeLabel(draft.type) }) : t('txt_edit_type_header', { type: cipherTypeLabel(draft.type) })}