feat: add export and import functionality for Bitwarden and NodeWarden formats

- Implemented export formats for Bitwarden (JSON, encrypted JSON, ZIP) and NodeWarden (JSON).
- Added support for attachments in ciphers and introduced new types for handling attachments.
- Enhanced import formats to include Bitwarden ZIP and NodeWarden JSON.
- Updated internationalization strings for attachment-related features.
- Improved UI styles for attachment management and import summary display.
This commit is contained in:
shuaiplus
2026-03-04 01:03:49 +08:00
parent 7b4733d4c4
commit 819734ce5c
15 changed files with 2379 additions and 75 deletions
+15 -1
View File
@@ -1,6 +1,6 @@
import { Env, Cipher, Folder, CipherType } from '../types';
import { StorageService } from '../services/storage';
import { errorResponse } from '../utils/response';
import { errorResponse, jsonResponse } from '../utils/response';
import { generateUUID } from '../utils/uuid';
import { LIMITS } from '../config/limits';
import { normalizeCipherLoginForCompatibility } from './ciphers';
@@ -8,6 +8,7 @@ import { normalizeCipherLoginForCompatibility } from './ciphers';
// Bitwarden client import request format
interface CiphersImportRequest {
ciphers: Array<{
id?: string | null;
type: number;
name?: string | null;
notes?: string | null;
@@ -90,6 +91,8 @@ async function runBatchInChunks(db: D1Database, statements: D1PreparedStatement[
// POST /api/ciphers/import - Bitwarden client import endpoint
export async function handleCiphersImport(request: Request, env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB);
const url = new URL(request.url);
const returnCipherMap = url.searchParams.get('returnCipherMap') === '1';
let importData: CiphersImportRequest;
try {
@@ -151,9 +154,12 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
// Create ciphers
const cipherRows: Cipher[] = [];
const cipherMapRows: Array<{ index: number; sourceId: string | null; id: string }> = [];
for (let i = 0; i < ciphers.length; i++) {
const c = ciphers[i];
const folderId = cipherFolderMap.get(i) || null;
const sourceIdRaw = String(c?.id ?? '').trim();
const sourceId = sourceIdRaw || null;
const cipher: Cipher = {
...c,
@@ -229,6 +235,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
cipher.login = normalizeCipherLoginForCompatibility(cipher.login);
cipherRows.push(cipher);
cipherMapRows.push({ index: i, sourceId, id: cipher.id });
}
if (cipherRows.length > 0) {
@@ -263,5 +270,12 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
// Update revision date
await storage.updateRevisionDate(userId);
if (returnCipherMap) {
return jsonResponse({
object: 'import-result',
cipherMap: cipherMapRows,
});
}
return new Response(null, { status: 200 });
}