From a00279f47d80d0da616f640fd7f7893720b61760 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 7 May 2026 19:36:32 +0800 Subject: [PATCH] feat: add domain settings support in backup import and export processes --- src/services/backup-archive.ts | 21 +++++++++++++++++++-- src/services/backup-import.ts | 20 +++++++++++++++++++- webapp/src/lib/api/backup.ts | 1 + 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/services/backup-archive.ts b/src/services/backup-archive.ts index 8db9bcb..0091584 100644 --- a/src/services/backup-archive.ts +++ b/src/services/backup-archive.ts @@ -51,6 +51,7 @@ export interface BackupPayload { db: { config: SqlRow[]; users: SqlRow[]; + domain_settings: SqlRow[]; user_revisions: SqlRow[]; folders: SqlRow[]; ciphers: SqlRow[]; @@ -284,6 +285,7 @@ export function validateBackupPayloadContents( const configRows = ensureRowArray(payload.db.config, 'config'); const userRows = ensureRowArray(payload.db.users, 'users'); const revisionRows = ensureRowArray(payload.db.user_revisions, 'user_revisions'); + const domainSettingsRows = ensureRowArray(payload.db.domain_settings || [], 'domain_settings'); const folderRows = ensureRowArray(payload.db.folders, 'folders'); const cipherRows = ensureRowArray(payload.db.ciphers, 'ciphers'); const attachmentRows = ensureRowArray(payload.db.attachments, 'attachments'); @@ -314,6 +316,18 @@ export function validateBackupPayloadContents( } } + const domainSettingUserIds = new Set(); + for (const row of domainSettingsRows) { + const userId = String(row.user_id || '').trim(); + if (!userId || !userIds.has(userId)) { + throw new Error(`Backup archive contains domain settings for an unknown user: ${userId || '(empty)'}`); + } + if (domainSettingUserIds.has(userId)) { + throw new Error(`Backup archive contains duplicate domain settings for user: ${userId}`); + } + domainSettingUserIds.add(userId); + } + const folderIds = new Set(); for (const row of folderRows) { const id = String(row.id || '').trim(); @@ -365,9 +379,10 @@ export async function buildBackupArchive( includeAttachments, }); const encoder = new TextEncoder(); - const [configRows, userRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([ + const [configRows, userRows, domainSettingsRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([ queryRows(env.DB, 'SELECT key, value FROM config ORDER BY key ASC'), - queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, api_key, created_at, updated_at FROM users ORDER BY created_at ASC'), + queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, created_at, updated_at FROM users ORDER BY created_at ASC'), + queryRows(env.DB, 'SELECT user_id, equivalent_domains, custom_equivalent_domains, excluded_global_equivalent_domains, updated_at FROM domain_settings ORDER BY user_id ASC'), queryRows(env.DB, 'SELECT user_id, revision_date FROM user_revisions ORDER BY user_id ASC'), queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'), @@ -394,6 +409,7 @@ export async function buildBackupArchive( tableCounts: { config: exportedConfigRows.length, users: userRows.length, + domain_settings: domainSettingsRows.length, user_revisions: revisionRows.length, folders: folderRows.length, ciphers: cipherRows.length, @@ -415,6 +431,7 @@ export async function buildBackupArchive( 'db.json': encoder.encode(JSON.stringify({ config: exportedConfigRows, users: userRows, + domain_settings: domainSettingsRows, user_revisions: revisionRows, folders: folderRows, ciphers: cipherRows, diff --git a/src/services/backup-import.ts b/src/services/backup-import.ts index 1addd94..34f3d47 100644 --- a/src/services/backup-import.ts +++ b/src/services/backup-import.ts @@ -12,6 +12,7 @@ type SqlRow = Record; type BackupTableName = | 'config' | 'users' + | 'domain_settings' | 'user_revisions' | 'folders' | 'ciphers' @@ -20,6 +21,7 @@ type BackupTableName = const BACKUP_TABLES: BackupTableName[] = [ 'config', 'users', + 'domain_settings', 'user_revisions', 'folders', 'ciphers', @@ -35,6 +37,7 @@ export interface BackupImportResultBody { imported: { config: number; users: number; + domainSettings: number; userRevisions: number; folders: number; ciphers: number; @@ -155,6 +158,7 @@ function buildResetImportTargetStatements(db: D1Database): D1PreparedStatement[] 'DELETE FROM attachments', 'DELETE FROM ciphers', 'DELETE FROM folders', + 'DELETE FROM domain_settings', 'DELETE FROM user_revisions', 'DELETE FROM users', 'DELETE FROM config', @@ -276,6 +280,7 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload[' ...row, verify_devices: row.verify_devices ?? 1, })), + domain_settings: cloneRows(payload.domain_settings || []), user_revisions: cloneRows(payload.user_revisions || []), folders: cloneRows(payload.folders || []), ciphers: cloneRows(payload.ciphers || []).map((row) => ({ @@ -594,7 +599,7 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us buildInsertStatements( db, tableName('users'), - ['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'api_key', 'created_at', 'updated_at'], + ['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'created_at', 'updated_at'], payload.users || [] ) ); @@ -603,6 +608,17 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us tableName('user_revisions'), buildInsertStatements(db, tableName('user_revisions'), ['user_id', 'revision_date'], payload.user_revisions || [], true) ); + await runInsertBatch( + db, + tableName('domain_settings'), + buildInsertStatements( + db, + tableName('domain_settings'), + ['user_id', 'equivalent_domains', 'custom_equivalent_domains', 'excluded_global_equivalent_domains', 'updated_at'], + payload.domain_settings || [], + true + ) + ); await runInsertBatch( db, tableName('folders'), @@ -729,6 +745,7 @@ export async function importBackupArchiveBytes( imported: { config: (db.config || []).length, users: (db.users || []).length, + domainSettings: (db.domain_settings || []).length, userRevisions: (db.user_revisions || []).length, folders: (db.folders || []).length, ciphers: (db.ciphers || []).length, @@ -870,6 +887,7 @@ export async function importRemoteBackupArchiveBytes( imported: { config: (db.config || []).length, users: (db.users || []).length, + domainSettings: (db.domain_settings || []).length, userRevisions: (db.user_revisions || []).length, folders: (db.folders || []).length, ciphers: (db.ciphers || []).length, diff --git a/webapp/src/lib/api/backup.ts b/webapp/src/lib/api/backup.ts index 98228ab..a37119b 100644 --- a/webapp/src/lib/api/backup.ts +++ b/webapp/src/lib/api/backup.ts @@ -94,6 +94,7 @@ export interface RemoteBackupBrowserResponse { export interface AdminBackupImportCounts { config: number; users: number; + domainSettings?: number; userRevisions: number; folders: number; ciphers: number;