From 5ab24ee8624795233e75fde2d7dafa2d2a33e509 Mon Sep 17 00:00:00 2001 From: nap0o Date: Tue, 3 Mar 2026 03:08:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E5=AF=BC=E5=85=A5csv?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 6 +++ backend/src/shared/db/db.ts | 3 +- frontend/index.html | 25 +++++---- .../migration/service/dataMigrationService.js | 33 ++++++++++-- .../features/migration/views/dataImport.vue | 21 ++++---- .../shared/utils/serializers/csvStrategy.js | 52 ++++++++++++++++--- frontend/vite.config.js | 4 +- 7 files changed, 107 insertions(+), 37 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ac3432..8a1e038 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -117,6 +117,12 @@ jobs: echo "$SECRETS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + - name: Mask Sensitive Domain + run: | + if [ -n "${{ secrets.CLOUDFLARE_WORKER_SUBDOMAIN }}" ]; then + echo "::add-mask::${{ secrets.CLOUDFLARE_WORKER_SUBDOMAIN }}" + fi + # 部署到 Cloudflare Workers - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 diff --git a/backend/src/shared/db/db.ts b/backend/src/shared/db/db.ts index b561eda..5a3becb 100644 --- a/backend/src/shared/db/db.ts +++ b/backend/src/shared/db/db.ts @@ -63,10 +63,11 @@ export async function batchInsertVaultItems( // 2. 批量写入:使用 D1 的 batch 功能避免连接超时。 // 分批写入,每批最多 50 条。 + // 使用 onConflictDoNothing() 作为兜底,遇到重复 (service, account) 时静默跳过而非抛出 500 const CHUNK_SIZE = 50; for (let i = 0; i < preparedItems.length; i += CHUNK_SIZE) { const chunk = preparedItems.slice(i, i + CHUNK_SIZE); - const stmts = chunk.map(item => dbClient.insert(schema.vault).values(item)); + const stmts = chunk.map(item => dbClient.insert(schema.vault).values(item).onConflictDoNothing()); await dbClient.batch(stmts as any); } diff --git a/frontend/index.html b/frontend/index.html index 3b9d674..8a924c8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,16 @@ - - - - - 2FAuth - Serverless 2FA Manager. - - -
- - - + + + + + + 2FAuth Worker - A Secure 2FA Management Tool. + + + +
+ + + + \ No newline at end of file diff --git a/frontend/src/features/migration/service/dataMigrationService.js b/frontend/src/features/migration/service/dataMigrationService.js index 97e481c..9c5d781 100644 --- a/frontend/src/features/migration/service/dataMigrationService.js +++ b/frontend/src/features/migration/service/dataMigrationService.js @@ -45,18 +45,28 @@ export const dataMigrationService = { return 'phonefactor' } + let textContent = content + if (content instanceof ArrayBuffer || content instanceof Uint8Array) { + try { + const bytes = content instanceof Uint8Array ? content : new Uint8Array(content) + textContent = new TextDecoder('utf-8', { fatal: false }).decode(bytes) + } catch (e) { + textContent = '' + } + } + if (filename && filename.toLowerCase().endsWith('.csv')) { - const firstLine = content.split('\n')[0].toLowerCase() + const firstLine = typeof textContent === 'string' ? textContent.split('\n')[0].toLowerCase() : '' if (firstLine.includes('login_totp')) return 'bwauth_csv' return 'generic_csv' } - if (typeof content === 'string' && content.trim().startsWith('otpauth://')) { + if (typeof textContent === 'string' && textContent.trim().startsWith('otpauth://')) { return 'text' } - if (typeof content === 'string') { - const json = tryParseJSON(content) + if (typeof textContent === 'string') { + const json = tryParseJSON(textContent) if (json) { if (Array.isArray(json.items) && json.items.length > 0 && json.items[0].login && json.items[0].login.totp) return 'bwauth_json' if (json.encrypted === true && json.app === '2fauth') return 'encrypted' @@ -446,7 +456,7 @@ export const dataMigrationService = { SQL = await initSqlJs() } const db = new SQL.Database(new Uint8Array(dbBuffer)) - + // 检查accounts表是否存在 const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'") if (tables.length === 0) { @@ -549,6 +559,19 @@ export const dataMigrationService = { return await this.parsePhoneFactor(content) } + // 统一处理将可能传入的 ArrayBuffer 转回 string,以便后续非数据库格式的解析 + let textContent = content + if (content instanceof ArrayBuffer || content instanceof Uint8Array) { + try { + const bytes = content instanceof Uint8Array ? content : new Uint8Array(content) + textContent = new TextDecoder('utf-8', { fatal: false }).decode(bytes) + } catch (e) { + console.warn('Failed to decode buffer as text', e) + } + } + // 对于非 phonefactor 的格式,后续逻辑全部使用 textContent 替代 content + content = textContent; + if (type === 'bwauth_csv' || type === 'generic_csv') { rawVault = csvStrategy.parseCsv(content) content = JSON.stringify(rawVault) diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index 9b4735d..35fc803 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -20,17 +20,12 @@ >
- 将备份文件或迁移二维码截图拖到此处,或 点击上传 +

💡 自动识别文件类型导入

+

将备份文件或迁移二维码截图拖到此处,或 点击上传