SECURITY FIX: Restore import/export functionality with KEK-DEK architecture
Fix critical missing functionality identified in security audit: ## New Features Implemented: ✅ User-level data export (encrypted/plaintext formats) ✅ User-level data import with dry-run validation ✅ Export preview endpoint for size estimation ✅ OIDC configuration encryption for sensitive data ✅ Production environment security checks on startup ## API Endpoints Restored: - POST /database/export - User data export with password protection - POST /database/import - User data import with validation - POST /database/export/preview - Export validation and stats ## Security Improvements: - OIDC client_secret now encrypted when admin data unlocked - Production startup checks for required environment variables - Comprehensive import/export documentation and examples - Proper error handling and cleanup for uploaded files ## Data Migration Support: - Cross-instance user data migration - Selective import (skip credentials/file manager data) - ID collision handling with automatic regeneration - Full validation of import data structure Resolves the critical "503 Service Unavailable" status on import/export endpoints that was blocking user data migration capabilities. Maintains KEK-DEK user-level encryption while enabling data portability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
216
src/backend/utils/import-export-test.ts
Normal file
216
src/backend/utils/import-export-test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { UserDataExport, type UserExportData } from "./user-data-export.js";
|
||||
import { UserDataImport, type ImportResult } from "./user-data-import.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
/**
|
||||
* 导入导出功能测试
|
||||
*
|
||||
* Linus原则:简单的冒烟测试,确保基本功能工作
|
||||
*/
|
||||
class ImportExportTest {
|
||||
|
||||
/**
|
||||
* 测试导出功能
|
||||
*/
|
||||
static async testExport(userId: string): Promise<boolean> {
|
||||
try {
|
||||
databaseLogger.info("Testing user data export functionality", {
|
||||
operation: "import_export_test",
|
||||
test: "export",
|
||||
userId,
|
||||
});
|
||||
|
||||
// 测试加密导出
|
||||
const encryptedExport = await UserDataExport.exportUserData(userId, {
|
||||
format: 'encrypted',
|
||||
scope: 'user_data',
|
||||
includeCredentials: true,
|
||||
});
|
||||
|
||||
// 验证导出数据结构
|
||||
const validation = UserDataExport.validateExportData(encryptedExport);
|
||||
if (!validation.valid) {
|
||||
databaseLogger.error("Export validation failed", {
|
||||
operation: "import_export_test",
|
||||
test: "export_validation",
|
||||
errors: validation.errors,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const stats = UserDataExport.getExportStats(encryptedExport);
|
||||
|
||||
databaseLogger.success("Export test completed successfully", {
|
||||
operation: "import_export_test",
|
||||
test: "export_success",
|
||||
totalRecords: stats.totalRecords,
|
||||
breakdown: stats.breakdown,
|
||||
encrypted: stats.encrypted,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Export test failed", error, {
|
||||
operation: "import_export_test",
|
||||
test: "export_failed",
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试导入功能(dry-run)
|
||||
*/
|
||||
static async testImportDryRun(userId: string, exportData: UserExportData): Promise<boolean> {
|
||||
try {
|
||||
databaseLogger.info("Testing user data import functionality (dry-run)", {
|
||||
operation: "import_export_test",
|
||||
test: "import_dry_run",
|
||||
userId,
|
||||
});
|
||||
|
||||
// 执行dry-run导入
|
||||
const result = await UserDataImport.importUserData(userId, exportData, {
|
||||
dryRun: true,
|
||||
replaceExisting: false,
|
||||
skipCredentials: false,
|
||||
skipFileManagerData: false,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
databaseLogger.success("Import dry-run test completed successfully", {
|
||||
operation: "import_export_test",
|
||||
test: "import_dry_run_success",
|
||||
summary: result.summary,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
databaseLogger.error("Import dry-run test failed", {
|
||||
operation: "import_export_test",
|
||||
test: "import_dry_run_failed",
|
||||
errors: result.summary.errors,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("Import dry-run test failed with exception", error, {
|
||||
operation: "import_export_test",
|
||||
test: "import_dry_run_exception",
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行完整的导入导出测试
|
||||
*/
|
||||
static async runFullTest(userId: string): Promise<boolean> {
|
||||
try {
|
||||
databaseLogger.info("Starting full import/export test suite", {
|
||||
operation: "import_export_test",
|
||||
test: "full_suite",
|
||||
userId,
|
||||
});
|
||||
|
||||
// 1. 测试导出
|
||||
const exportSuccess = await this.testExport(userId);
|
||||
if (!exportSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取导出数据用于导入测试
|
||||
const exportData = await UserDataExport.exportUserData(userId, {
|
||||
format: 'encrypted',
|
||||
scope: 'user_data',
|
||||
includeCredentials: true,
|
||||
});
|
||||
|
||||
// 3. 测试导入(dry-run)
|
||||
const importSuccess = await this.testImportDryRun(userId, exportData);
|
||||
if (!importSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
databaseLogger.success("Full import/export test suite completed successfully", {
|
||||
operation: "import_export_test",
|
||||
test: "full_suite_success",
|
||||
userId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Full import/export test suite failed", error, {
|
||||
operation: "import_export_test",
|
||||
test: "full_suite_failed",
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JSON序列化和反序列化
|
||||
*/
|
||||
static async testJSONSerialization(userId: string): Promise<boolean> {
|
||||
try {
|
||||
databaseLogger.info("Testing JSON serialization/deserialization", {
|
||||
operation: "import_export_test",
|
||||
test: "json_serialization",
|
||||
userId,
|
||||
});
|
||||
|
||||
// 导出为JSON字符串
|
||||
const jsonString = await UserDataExport.exportUserDataToJSON(userId, {
|
||||
format: 'encrypted',
|
||||
pretty: true,
|
||||
});
|
||||
|
||||
// 解析JSON
|
||||
const parsedData = JSON.parse(jsonString);
|
||||
|
||||
// 验证解析后的数据
|
||||
const validation = UserDataExport.validateExportData(parsedData);
|
||||
if (!validation.valid) {
|
||||
databaseLogger.error("JSON serialization validation failed", {
|
||||
operation: "import_export_test",
|
||||
test: "json_validation_failed",
|
||||
errors: validation.errors,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 测试从JSON导入(dry-run)
|
||||
const importResult = await UserDataImport.importUserDataFromJSON(userId, jsonString, {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
if (importResult.success) {
|
||||
databaseLogger.success("JSON serialization test completed successfully", {
|
||||
operation: "import_export_test",
|
||||
test: "json_serialization_success",
|
||||
jsonSize: jsonString.length,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
databaseLogger.error("JSON import test failed", {
|
||||
operation: "import_export_test",
|
||||
test: "json_import_failed",
|
||||
errors: importResult.summary.errors,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("JSON serialization test failed", error, {
|
||||
operation: "import_export_test",
|
||||
test: "json_serialization_exception",
|
||||
userId,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ImportExportTest };
|
||||
250
src/backend/utils/user-data-export.ts
Normal file
250
src/backend/utils/user-data-export.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { db } from "../database/db/index.js";
|
||||
import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts } from "../database/db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
interface UserExportData {
|
||||
version: string;
|
||||
exportedAt: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
userData: {
|
||||
sshHosts: any[];
|
||||
sshCredentials: any[];
|
||||
fileManagerData: {
|
||||
recent: any[];
|
||||
pinned: any[];
|
||||
shortcuts: any[];
|
||||
};
|
||||
dismissedAlerts: any[];
|
||||
};
|
||||
metadata: {
|
||||
totalRecords: number;
|
||||
encrypted: boolean;
|
||||
exportType: 'user_data' | 'system_config' | 'all';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDataExport - 用户级数据导入导出
|
||||
*
|
||||
* Linus原则:
|
||||
* - 用户拥有自己的数据,应该能自由导出
|
||||
* - 简单直接,没有复杂的权限检查
|
||||
* - 支持加密和明文两种格式
|
||||
* - 不破坏现有系统架构
|
||||
*/
|
||||
class UserDataExport {
|
||||
private static readonly EXPORT_VERSION = "v2.0";
|
||||
|
||||
/**
|
||||
* 导出用户数据
|
||||
*/
|
||||
static async exportUserData(
|
||||
userId: string,
|
||||
options: {
|
||||
format?: 'encrypted' | 'plaintext';
|
||||
scope?: 'user_data' | 'all';
|
||||
includeCredentials?: boolean;
|
||||
} = {}
|
||||
): Promise<UserExportData> {
|
||||
const { format = 'encrypted', scope = 'user_data', includeCredentials = true } = options;
|
||||
|
||||
try {
|
||||
databaseLogger.info("Starting user data export", {
|
||||
operation: "user_data_export",
|
||||
userId,
|
||||
format,
|
||||
scope,
|
||||
includeCredentials,
|
||||
});
|
||||
|
||||
// 验证用户存在
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0) {
|
||||
throw new Error(`User not found: ${userId}`);
|
||||
}
|
||||
|
||||
const userRecord = user[0];
|
||||
|
||||
// 获取用户数据密钥(如果需要解密)
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (format === 'plaintext') {
|
||||
userDataKey = DataCrypto.getUserDataKey(userId);
|
||||
if (!userDataKey) {
|
||||
throw new Error("User data not unlocked - password required for plaintext export");
|
||||
}
|
||||
}
|
||||
|
||||
// 导出SSH主机配置
|
||||
const sshHosts = await db.select().from(sshData).where(eq(sshData.userId, userId));
|
||||
const processedSshHosts = format === 'plaintext' && userDataKey
|
||||
? sshHosts.map(host => DataCrypto.decryptRecord("ssh_data", host, userId, userDataKey!))
|
||||
: sshHosts;
|
||||
|
||||
// 导出SSH凭据(如果包含)
|
||||
let sshCredentialsData: any[] = [];
|
||||
if (includeCredentials) {
|
||||
const credentials = await db.select().from(sshCredentials).where(eq(sshCredentials.userId, userId));
|
||||
sshCredentialsData = format === 'plaintext' && userDataKey
|
||||
? credentials.map(cred => DataCrypto.decryptRecord("ssh_credentials", cred, userId, userDataKey!))
|
||||
: credentials;
|
||||
}
|
||||
|
||||
// 导出文件管理器数据
|
||||
const [recentFiles, pinnedFiles, shortcuts] = await Promise.all([
|
||||
db.select().from(fileManagerRecent).where(eq(fileManagerRecent.userId, userId)),
|
||||
db.select().from(fileManagerPinned).where(eq(fileManagerPinned.userId, userId)),
|
||||
db.select().from(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, userId)),
|
||||
]);
|
||||
|
||||
// 导出已忽略的警告
|
||||
const alerts = await db.select().from(dismissedAlerts).where(eq(dismissedAlerts.userId, userId));
|
||||
|
||||
// 构建导出数据
|
||||
const exportData: UserExportData = {
|
||||
version: this.EXPORT_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
userId: userRecord.id,
|
||||
username: userRecord.username,
|
||||
userData: {
|
||||
sshHosts: processedSshHosts,
|
||||
sshCredentials: sshCredentialsData,
|
||||
fileManagerData: {
|
||||
recent: recentFiles,
|
||||
pinned: pinnedFiles,
|
||||
shortcuts: shortcuts,
|
||||
},
|
||||
dismissedAlerts: alerts,
|
||||
},
|
||||
metadata: {
|
||||
totalRecords: processedSshHosts.length + sshCredentialsData.length + recentFiles.length + pinnedFiles.length + shortcuts.length + alerts.length,
|
||||
encrypted: format === 'encrypted',
|
||||
exportType: scope,
|
||||
},
|
||||
};
|
||||
|
||||
databaseLogger.success("User data export completed", {
|
||||
operation: "user_data_export_complete",
|
||||
userId,
|
||||
totalRecords: exportData.metadata.totalRecords,
|
||||
format,
|
||||
sshHosts: processedSshHosts.length,
|
||||
sshCredentials: sshCredentialsData.length,
|
||||
});
|
||||
|
||||
return exportData;
|
||||
} catch (error) {
|
||||
databaseLogger.error("User data export failed", error, {
|
||||
operation: "user_data_export_failed",
|
||||
userId,
|
||||
format,
|
||||
scope,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为JSON字符串
|
||||
*/
|
||||
static async exportUserDataToJSON(
|
||||
userId: string,
|
||||
options: {
|
||||
format?: 'encrypted' | 'plaintext';
|
||||
scope?: 'user_data' | 'all';
|
||||
includeCredentials?: boolean;
|
||||
pretty?: boolean;
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
const { pretty = true } = options;
|
||||
const exportData = await this.exportUserData(userId, options);
|
||||
return JSON.stringify(exportData, null, pretty ? 2 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证导出数据格式
|
||||
*/
|
||||
static validateExportData(data: any): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
errors.push("Export data must be an object");
|
||||
return { valid: false, errors };
|
||||
}
|
||||
|
||||
if (!data.version) {
|
||||
errors.push("Missing version field");
|
||||
}
|
||||
|
||||
if (!data.userId) {
|
||||
errors.push("Missing userId field");
|
||||
}
|
||||
|
||||
if (!data.userData || typeof data.userData !== 'object') {
|
||||
errors.push("Missing or invalid userData field");
|
||||
}
|
||||
|
||||
if (!data.metadata || typeof data.metadata !== 'object') {
|
||||
errors.push("Missing or invalid metadata field");
|
||||
}
|
||||
|
||||
// 检查必需的数据字段
|
||||
if (data.userData) {
|
||||
const requiredFields = ['sshHosts', 'sshCredentials', 'fileManagerData', 'dismissedAlerts'];
|
||||
for (const field of requiredFields) {
|
||||
if (!Array.isArray(data.userData[field]) && !(field === 'fileManagerData' && typeof data.userData[field] === 'object')) {
|
||||
errors.push(`Missing or invalid userData.${field} field`);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.userData.fileManagerData && typeof data.userData.fileManagerData === 'object') {
|
||||
const fmFields = ['recent', 'pinned', 'shortcuts'];
|
||||
for (const field of fmFields) {
|
||||
if (!Array.isArray(data.userData.fileManagerData[field])) {
|
||||
errors.push(`Missing or invalid userData.fileManagerData.${field} field`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导出数据统计信息
|
||||
*/
|
||||
static getExportStats(data: UserExportData): {
|
||||
version: string;
|
||||
exportedAt: string;
|
||||
username: string;
|
||||
totalRecords: number;
|
||||
breakdown: {
|
||||
sshHosts: number;
|
||||
sshCredentials: number;
|
||||
fileManagerItems: number;
|
||||
dismissedAlerts: number;
|
||||
};
|
||||
encrypted: boolean;
|
||||
} {
|
||||
return {
|
||||
version: data.version,
|
||||
exportedAt: data.exportedAt,
|
||||
username: data.username,
|
||||
totalRecords: data.metadata.totalRecords,
|
||||
breakdown: {
|
||||
sshHosts: data.userData.sshHosts.length,
|
||||
sshCredentials: data.userData.sshCredentials.length,
|
||||
fileManagerItems: data.userData.fileManagerData.recent.length +
|
||||
data.userData.fileManagerData.pinned.length +
|
||||
data.userData.fileManagerData.shortcuts.length,
|
||||
dismissedAlerts: data.userData.dismissedAlerts.length,
|
||||
},
|
||||
encrypted: data.metadata.encrypted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { UserDataExport, type UserExportData };
|
||||
424
src/backend/utils/user-data-import.ts
Normal file
424
src/backend/utils/user-data-import.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { db } from "../database/db/index.js";
|
||||
import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import { UserDataExport, type UserExportData } from "./user-data-export.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface ImportOptions {
|
||||
replaceExisting?: boolean;
|
||||
skipCredentials?: boolean;
|
||||
skipFileManagerData?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean;
|
||||
summary: {
|
||||
sshHostsImported: number;
|
||||
sshCredentialsImported: number;
|
||||
fileManagerItemsImported: number;
|
||||
dismissedAlertsImported: number;
|
||||
skippedItems: number;
|
||||
errors: string[];
|
||||
};
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDataImport - 用户数据导入
|
||||
*
|
||||
* Linus原则:
|
||||
* - 导入不应该破坏现有数据(除非明确要求)
|
||||
* - 支持dry-run模式验证
|
||||
* - 处理ID冲突的简单策略:重新生成
|
||||
* - 错误处理要明确,不能静默失败
|
||||
*/
|
||||
class UserDataImport {
|
||||
|
||||
/**
|
||||
* 导入用户数据
|
||||
*/
|
||||
static async importUserData(
|
||||
targetUserId: string,
|
||||
exportData: UserExportData,
|
||||
options: ImportOptions = {}
|
||||
): Promise<ImportResult> {
|
||||
const {
|
||||
replaceExisting = false,
|
||||
skipCredentials = false,
|
||||
skipFileManagerData = false,
|
||||
dryRun = false
|
||||
} = options;
|
||||
|
||||
try {
|
||||
databaseLogger.info("Starting user data import", {
|
||||
operation: "user_data_import",
|
||||
targetUserId,
|
||||
sourceUserId: exportData.userId,
|
||||
sourceUsername: exportData.username,
|
||||
dryRun,
|
||||
replaceExisting,
|
||||
skipCredentials,
|
||||
skipFileManagerData,
|
||||
});
|
||||
|
||||
// 验证目标用户存在
|
||||
const targetUser = await db.select().from(users).where(eq(users.id, targetUserId));
|
||||
if (!targetUser || targetUser.length === 0) {
|
||||
throw new Error(`Target user not found: ${targetUserId}`);
|
||||
}
|
||||
|
||||
// 验证导出数据格式
|
||||
const validation = UserDataExport.validateExportData(exportData);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid export data: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// 验证用户数据已解锁(如果数据是加密的)
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (exportData.metadata.encrypted) {
|
||||
userDataKey = DataCrypto.getUserDataKey(targetUserId);
|
||||
if (!userDataKey) {
|
||||
throw new Error("Target user data not unlocked - password required for encrypted import");
|
||||
}
|
||||
}
|
||||
|
||||
const result: ImportResult = {
|
||||
success: false,
|
||||
summary: {
|
||||
sshHostsImported: 0,
|
||||
sshCredentialsImported: 0,
|
||||
fileManagerItemsImported: 0,
|
||||
dismissedAlertsImported: 0,
|
||||
skippedItems: 0,
|
||||
errors: [],
|
||||
},
|
||||
dryRun,
|
||||
};
|
||||
|
||||
// 导入SSH主机配置
|
||||
if (exportData.userData.sshHosts && exportData.userData.sshHosts.length > 0) {
|
||||
const importStats = await this.importSshHosts(
|
||||
targetUserId,
|
||||
exportData.userData.sshHosts,
|
||||
{ replaceExisting, dryRun, userDataKey }
|
||||
);
|
||||
result.summary.sshHostsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入SSH凭据
|
||||
if (!skipCredentials && exportData.userData.sshCredentials && exportData.userData.sshCredentials.length > 0) {
|
||||
const importStats = await this.importSshCredentials(
|
||||
targetUserId,
|
||||
exportData.userData.sshCredentials,
|
||||
{ replaceExisting, dryRun, userDataKey }
|
||||
);
|
||||
result.summary.sshCredentialsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入文件管理器数据
|
||||
if (!skipFileManagerData && exportData.userData.fileManagerData) {
|
||||
const importStats = await this.importFileManagerData(
|
||||
targetUserId,
|
||||
exportData.userData.fileManagerData,
|
||||
{ replaceExisting, dryRun }
|
||||
);
|
||||
result.summary.fileManagerItemsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入忽略的警告
|
||||
if (exportData.userData.dismissedAlerts && exportData.userData.dismissedAlerts.length > 0) {
|
||||
const importStats = await this.importDismissedAlerts(
|
||||
targetUserId,
|
||||
exportData.userData.dismissedAlerts,
|
||||
{ replaceExisting, dryRun }
|
||||
);
|
||||
result.summary.dismissedAlertsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
result.success = result.summary.errors.length === 0;
|
||||
|
||||
databaseLogger.success("User data import completed", {
|
||||
operation: "user_data_import_complete",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
...result.summary,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
databaseLogger.error("User data import failed", error, {
|
||||
operation: "user_data_import_failed",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入SSH主机配置
|
||||
*/
|
||||
private static async importSshHosts(
|
||||
targetUserId: string,
|
||||
sshHosts: any[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean; userDataKey: Buffer | null }
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const host of sshHosts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新生成ID避免冲突
|
||||
const newHostData = {
|
||||
...host,
|
||||
id: undefined, // 让数据库自动生成
|
||||
userId: targetUserId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 如果数据需要重新加密
|
||||
let processedHostData = newHostData;
|
||||
if (options.userDataKey) {
|
||||
processedHostData = DataCrypto.encryptRecord("ssh_data", newHostData, targetUserId, options.userDataKey);
|
||||
}
|
||||
|
||||
await db.insert(sshData).values(processedHostData);
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`SSH host import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入SSH凭据
|
||||
*/
|
||||
private static async importSshCredentials(
|
||||
targetUserId: string,
|
||||
credentials: any[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean; userDataKey: Buffer | null }
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const credential of credentials) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新生成ID避免冲突
|
||||
const newCredentialData = {
|
||||
...credential,
|
||||
id: undefined, // 让数据库自动生成
|
||||
userId: targetUserId,
|
||||
usageCount: 0, // 重置使用计数
|
||||
lastUsed: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 如果数据需要重新加密
|
||||
let processedCredentialData = newCredentialData;
|
||||
if (options.userDataKey) {
|
||||
processedCredentialData = DataCrypto.encryptRecord("ssh_credentials", newCredentialData, targetUserId, options.userDataKey);
|
||||
}
|
||||
|
||||
await db.insert(sshCredentials).values(processedCredentialData);
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`SSH credential import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入文件管理器数据
|
||||
*/
|
||||
private static async importFileManagerData(
|
||||
targetUserId: string,
|
||||
fileManagerData: any,
|
||||
options: { replaceExisting: boolean; dryRun: boolean }
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
// 导入最近文件
|
||||
if (fileManagerData.recent && Array.isArray(fileManagerData.recent)) {
|
||||
for (const item of fileManagerData.recent) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
await db.insert(fileManagerRecent).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`Recent file import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导入固定文件
|
||||
if (fileManagerData.pinned && Array.isArray(fileManagerData.pinned)) {
|
||||
for (const item of fileManagerData.pinned) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
pinnedAt: new Date().toISOString(),
|
||||
};
|
||||
await db.insert(fileManagerPinned).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`Pinned file import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导入快捷方式
|
||||
if (fileManagerData.shortcuts && Array.isArray(fileManagerData.shortcuts)) {
|
||||
for (const item of fileManagerData.shortcuts) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await db.insert(fileManagerShortcuts).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`Shortcut import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`File manager data import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入忽略的警告
|
||||
*/
|
||||
private static async importDismissedAlerts(
|
||||
targetUserId: string,
|
||||
alerts: any[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean }
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const alert of alerts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的警告
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(dismissedAlerts)
|
||||
.where(
|
||||
and(
|
||||
eq(dismissedAlerts.userId, targetUserId),
|
||||
eq(dismissedAlerts.alertId, alert.alertId)
|
||||
)
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
...alert,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
dismissedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await db
|
||||
.update(dismissedAlerts)
|
||||
.set(newAlert)
|
||||
.where(eq(dismissedAlerts.id, existing[0].id));
|
||||
} else {
|
||||
await db.insert(dismissedAlerts).values(newAlert);
|
||||
}
|
||||
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(`Dismissed alert import failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON字符串导入
|
||||
*/
|
||||
static async importUserDataFromJSON(
|
||||
targetUserId: string,
|
||||
jsonData: string,
|
||||
options: ImportOptions = {}
|
||||
): Promise<ImportResult> {
|
||||
try {
|
||||
const exportData: UserExportData = JSON.parse(jsonData);
|
||||
return await this.importUserData(targetUserId, exportData, options);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("Invalid JSON format in import data");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UserDataImport, type ImportOptions, type ImportResult };
|
||||
Reference in New Issue
Block a user