Clean Chinese comments from backend codebase
Replace all Chinese comments with English equivalents while preserving: - Technical meaning and Linus-style direct tone - Code structure and functionality - User-facing text in UI components Backend files cleaned: - All utils/ TypeScript files - Database routes and operations - System architecture comments - Field encryption documentation All backend code now uses consistent English comments.
This commit is contained in:
@@ -412,7 +412,7 @@ app.post("/database/export", async (req, res) => {
|
||||
const userId = payload.userId;
|
||||
const { format = 'encrypted', scope = 'user_data', includeCredentials = true, password } = req.body;
|
||||
|
||||
// 对于明文导出,需要解锁用户数据
|
||||
// For plaintext export, need to unlock user data
|
||||
if (format === 'plaintext') {
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
@@ -441,7 +441,7 @@ app.post("/database/export", async (req, res) => {
|
||||
includeCredentials,
|
||||
});
|
||||
|
||||
// 生成导出文件名
|
||||
// Generate export filename
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const filename = `termix-export-${exportData.username}-${timestamp}.json`;
|
||||
|
||||
@@ -507,10 +507,10 @@ app.post("/database/import", upload.single("file"), async (req, res) => {
|
||||
dryRun,
|
||||
});
|
||||
|
||||
// 读取上传的文件
|
||||
// Read uploaded file
|
||||
const fileContent = fs.readFileSync(req.file.path, 'utf8');
|
||||
|
||||
// 清理上传的临时文件
|
||||
// Clean up uploaded temporary file
|
||||
try {
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupError) {
|
||||
@@ -520,7 +520,7 @@ app.post("/database/import", upload.single("file"), async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 解析导入数据
|
||||
// Parse import data
|
||||
let importData;
|
||||
try {
|
||||
importData = JSON.parse(fileContent);
|
||||
@@ -528,7 +528,7 @@ app.post("/database/import", upload.single("file"), async (req, res) => {
|
||||
return res.status(400).json({ error: "Invalid JSON format in uploaded file" });
|
||||
}
|
||||
|
||||
// 如果导入数据是加密的,需要解锁用户数据
|
||||
// If import data is encrypted, need to unlock user data
|
||||
if (importData.metadata?.encrypted) {
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
@@ -543,7 +543,7 @@ app.post("/database/import", upload.single("file"), async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 执行导入
|
||||
// Execute import
|
||||
const result = await UserDataImport.importUserData(userId, importData, {
|
||||
replaceExisting: replaceExisting === 'true' || replaceExisting === true,
|
||||
skipCredentials: skipCredentials === 'true' || skipCredentials === true,
|
||||
@@ -619,9 +619,9 @@ app.post("/database/export/preview", async (req, res) => {
|
||||
includeCredentials,
|
||||
});
|
||||
|
||||
// 生成导出数据但不解密敏感字段
|
||||
// Generate export data but don't decrypt sensitive fields
|
||||
const exportData = await UserDataExport.exportUserData(userId, {
|
||||
format: 'encrypted', // 始终加密预览
|
||||
format: 'encrypted', // Always encrypt preview
|
||||
scope,
|
||||
includeCredentials,
|
||||
});
|
||||
|
||||
@@ -336,10 +336,10 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
scopes: scopes || "openid email profile",
|
||||
};
|
||||
|
||||
// 对敏感配置进行加密存储
|
||||
// Encrypt sensitive configuration for storage
|
||||
let encryptedConfig;
|
||||
try {
|
||||
// 使用管理员的数据密钥加密OIDC配置
|
||||
// Use admin's data key to encrypt OIDC configuration
|
||||
const adminDataKey = DataCrypto.getUserDataKey(userId);
|
||||
if (adminDataKey) {
|
||||
encryptedConfig = DataCrypto.encryptRecord("settings", config, userId, adminDataKey);
|
||||
@@ -348,10 +348,10 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
userId,
|
||||
});
|
||||
} else {
|
||||
// 如果管理员数据未解锁,只加密client_secret
|
||||
// If admin data not unlocked, only encrypt client_secret
|
||||
encryptedConfig = {
|
||||
...config,
|
||||
client_secret: `encrypted:${Buffer.from(client_secret).toString('base64')}`, // 简单的base64编码
|
||||
client_secret: `encrypted:${Buffer.from(client_secret).toString('base64')}`, // Simple base64 encoding
|
||||
};
|
||||
authLogger.warn("OIDC configuration stored with basic encoding - admin should re-save with password", {
|
||||
operation: "oidc_config_basic_encoding",
|
||||
@@ -422,10 +422,10 @@ router.get("/oidc-config", async (req, res) => {
|
||||
|
||||
let config = JSON.parse((row as any).value);
|
||||
|
||||
// 解密或解码client_secret用于显示
|
||||
// Decrypt or decode client_secret for display
|
||||
if (config.client_secret) {
|
||||
if (config.client_secret.startsWith('encrypted:')) {
|
||||
// 需要管理员权限解密
|
||||
// Requires admin permission to decrypt
|
||||
const authHeader = req.headers["authorization"];
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const token = authHeader.split(" ")[1];
|
||||
@@ -442,7 +442,7 @@ router.get("/oidc-config", async (req, res) => {
|
||||
if (adminDataKey) {
|
||||
config = DataCrypto.decryptRecord("settings", config, userId, adminDataKey);
|
||||
} else {
|
||||
// 管理员数据未解锁,隐藏client_secret
|
||||
// Admin data not unlocked, hide client_secret
|
||||
config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
|
||||
}
|
||||
} catch (decryptError) {
|
||||
@@ -462,7 +462,7 @@ router.get("/oidc-config", async (req, res) => {
|
||||
config.client_secret = "[ENCRYPTED - AUTH REQUIRED]";
|
||||
}
|
||||
} else if (config.client_secret.startsWith('encoded:')) {
|
||||
// base64解码
|
||||
// base64 decode
|
||||
try {
|
||||
const decoded = Buffer.from(config.client_secret.substring(8), 'base64').toString('utf8');
|
||||
config.client_secret = decoded;
|
||||
@@ -470,7 +470,7 @@ router.get("/oidc-config", async (req, res) => {
|
||||
config.client_secret = "[ENCODING ERROR]";
|
||||
}
|
||||
}
|
||||
// 否则是明文,直接返回
|
||||
// Otherwise plaintext, return directly
|
||||
}
|
||||
|
||||
res.json(config);
|
||||
|
||||
@@ -15,7 +15,7 @@ import "dotenv/config";
|
||||
version: version,
|
||||
});
|
||||
|
||||
// 生产环境安全检查
|
||||
// Production environment security checks
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
systemLogger.info("Running production environment security checks...", {
|
||||
operation: "security_checks",
|
||||
@@ -23,19 +23,19 @@ import "dotenv/config";
|
||||
|
||||
const securityIssues: string[] = [];
|
||||
|
||||
// 检查系统主密钥
|
||||
// Check system master key
|
||||
if (!process.env.SYSTEM_MASTER_KEY) {
|
||||
securityIssues.push("SYSTEM_MASTER_KEY environment variable is required in production");
|
||||
} else if (process.env.SYSTEM_MASTER_KEY.length < 64) {
|
||||
securityIssues.push("SYSTEM_MASTER_KEY should be at least 64 characters in production");
|
||||
}
|
||||
|
||||
// 检查数据库文件加密
|
||||
// Check database file encryption
|
||||
if (process.env.DB_FILE_ENCRYPTION === 'false') {
|
||||
securityIssues.push("Database file encryption should be enabled in production");
|
||||
}
|
||||
|
||||
// 检查JWT移密
|
||||
// Check JWT secret
|
||||
if (!process.env.JWT_SECRET) {
|
||||
systemLogger.info("JWT_SECRET not set - will use encrypted storage", {
|
||||
operation: "security_checks",
|
||||
@@ -43,7 +43,7 @@ import "dotenv/config";
|
||||
});
|
||||
}
|
||||
|
||||
// 检查CORS配置警告
|
||||
// Check CORS configuration warning
|
||||
systemLogger.warn("Production deployment detected - ensure CORS is properly configured", {
|
||||
operation: "security_checks",
|
||||
warning: "Verify frontend domain whitelist"
|
||||
|
||||
@@ -23,14 +23,14 @@ interface JWTPayload {
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthManager - 简化的认证管理器
|
||||
* AuthManager - Simplified authentication manager
|
||||
*
|
||||
* 职责:
|
||||
* - JWT生成和验证
|
||||
* - 认证中间件
|
||||
* - 用户登录登出
|
||||
* Responsibilities:
|
||||
* - JWT generation and validation
|
||||
* - Authentication middleware
|
||||
* - User login/logout
|
||||
*
|
||||
* 不再有两层session - 直接使用UserKeyManager
|
||||
* No more two-layer sessions - use UserKeyManager directly
|
||||
*/
|
||||
class AuthManager {
|
||||
private static instance: AuthManager;
|
||||
@@ -50,7 +50,7 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化认证系统
|
||||
* Initialize authentication system
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.systemCrypto.initializeJWTSecret();
|
||||
@@ -60,21 +60,21 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* User registration
|
||||
*/
|
||||
async registerUser(userId: string, password: string): Promise<void> {
|
||||
await this.userCrypto.setupUserEncryption(userId, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录 - 使用UserCrypto
|
||||
* User login - use UserCrypto
|
||||
*/
|
||||
async authenticateUser(userId: string, password: string): Promise<boolean> {
|
||||
return await this.userCrypto.authenticateUser(userId, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JWT Token
|
||||
* Generate JWT Token
|
||||
*/
|
||||
async generateJWTToken(
|
||||
userId: string,
|
||||
@@ -93,7 +93,7 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT Token
|
||||
* Verify JWT Token
|
||||
*/
|
||||
async verifyJWTToken(token: string): Promise<JWTPayload | null> {
|
||||
try {
|
||||
@@ -105,7 +105,7 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
* Authentication middleware
|
||||
*/
|
||||
createAuthMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -128,7 +128,7 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据访问中间件 - 要求用户已解锁数据
|
||||
* Data access middleware - requires user to have unlocked data
|
||||
*/
|
||||
createDataAccessMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -151,28 +151,28 @@ class AuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* User logout
|
||||
*/
|
||||
logoutUser(userId: string): void {
|
||||
this.userCrypto.logoutUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据密钥
|
||||
* Get user data key
|
||||
*/
|
||||
getUserDataKey(userId: string): Buffer | null {
|
||||
return this.userCrypto.getUserDataKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已解锁
|
||||
* Check if user is unlocked
|
||||
*/
|
||||
isUserUnlocked(userId: string): boolean {
|
||||
return this.userCrypto.isUserUnlocked(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户密码
|
||||
* Change user password
|
||||
*/
|
||||
async changeUserPassword(userId: string, oldPassword: string, newPassword: string): Promise<boolean> {
|
||||
return await this.userCrypto.changeUserPassword(userId, oldPassword, newPassword);
|
||||
|
||||
@@ -3,13 +3,13 @@ import { UserCrypto } from "./user-crypto.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
/**
|
||||
* DataCrypto - 简化的数据库加密
|
||||
* DataCrypto - Simplified database encryption
|
||||
*
|
||||
* Linus原则:
|
||||
* - 删除所有"向后兼容"垃圾
|
||||
* - 删除所有特殊情况处理
|
||||
* - 数据要么正确加密,要么操作失败
|
||||
* - 没有legacy data概念
|
||||
* Linus principles:
|
||||
* - Remove all "backward compatibility" garbage
|
||||
* - Remove all special case handling
|
||||
* - Data is either properly encrypted or operation fails
|
||||
* - No legacy data concept
|
||||
*/
|
||||
class DataCrypto {
|
||||
private static userCrypto: UserCrypto;
|
||||
@@ -43,12 +43,12 @@ class DataCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密记录 - 要么成功,要么失败
|
||||
* Decrypt record - either succeeds or fails
|
||||
*
|
||||
* 删除了所有的:
|
||||
* - isEncrypted()检查
|
||||
* - legacy data处理
|
||||
* - "向后兼容"逻辑
|
||||
* Removed all:
|
||||
* - isEncrypted() checks
|
||||
* - legacy data handling
|
||||
* - "backward compatibility" logic
|
||||
* - migration on access
|
||||
*/
|
||||
static decryptRecord(tableName: string, record: any, userId: string, userDataKey: Buffer): any {
|
||||
@@ -59,8 +59,8 @@ class DataCrypto {
|
||||
|
||||
for (const [fieldName, value] of Object.entries(record)) {
|
||||
if (FieldCrypto.shouldEncryptField(tableName, fieldName) && value) {
|
||||
// 简单规则:敏感字段必须是加密的JSON格式
|
||||
// 如果不是,就是数据损坏,直接失败
|
||||
// Simple rule: sensitive fields must be encrypted JSON format
|
||||
// If not, it's data corruption, fail directly
|
||||
decryptedRecord[fieldName] = FieldCrypto.decryptField(
|
||||
value as string,
|
||||
userDataKey,
|
||||
|
||||
@@ -8,13 +8,13 @@ interface EncryptedData {
|
||||
}
|
||||
|
||||
/**
|
||||
* FieldCrypto - 简单直接的字段加密
|
||||
* FieldCrypto - Simple direct field encryption
|
||||
*
|
||||
* Linus原则:
|
||||
* - 没有特殊情况
|
||||
* - 没有兼容性检查
|
||||
* - 数据要么加密,要么失败
|
||||
* - 不存在"legacy data"概念
|
||||
* Linus principles:
|
||||
* - No special cases
|
||||
* - No compatibility checks
|
||||
* - Data is either encrypted or fails
|
||||
* - No "legacy data" concept
|
||||
*/
|
||||
class FieldCrypto {
|
||||
private static readonly ALGORITHM = "aes-256-gcm";
|
||||
@@ -22,7 +22,7 @@ class FieldCrypto {
|
||||
private static readonly IV_LENGTH = 16;
|
||||
private static readonly SALT_LENGTH = 32;
|
||||
|
||||
// 需要加密的字段 - 简单的映射,没有复杂逻辑
|
||||
// Fields requiring encryption - simple mapping, no complex logic
|
||||
private static readonly ENCRYPTED_FIELDS = {
|
||||
users: new Set(["password_hash", "client_secret", "totp_secret", "totp_backup_codes", "oidc_identifier"]),
|
||||
ssh_data: new Set(["password", "key", "keyPassword"]),
|
||||
@@ -30,7 +30,7 @@ class FieldCrypto {
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密字段 - 没有特殊情况
|
||||
* Encrypt field - no special cases
|
||||
*/
|
||||
static encryptField(plaintext: string, masterKey: Buffer, recordId: string, fieldName: string): string {
|
||||
if (!plaintext) return "";
|
||||
@@ -57,7 +57,7 @@ class FieldCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字段 - 要么成功,要么失败,没有第三种情况
|
||||
* Decrypt field - either succeeds or fails, no third option
|
||||
*/
|
||||
static decryptField(encryptedValue: string, masterKey: Buffer, recordId: string, fieldName: string): string {
|
||||
if (!encryptedValue) return "";
|
||||
@@ -77,7 +77,7 @@ class FieldCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字段是否需要加密 - 简单查表,没有复杂逻辑
|
||||
* Check if field needs encryption - simple table lookup, no complex logic
|
||||
*/
|
||||
static shouldEncryptField(tableName: string, fieldName: string): boolean {
|
||||
const fields = this.ENCRYPTED_FIELDS[tableName as keyof typeof this.ENCRYPTED_FIELDS];
|
||||
|
||||
@@ -6,17 +6,17 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
type TableName = "users" | "ssh_data" | "ssh_credentials";
|
||||
|
||||
/**
|
||||
* SimpleDBOps - 简化的加密数据库操作
|
||||
* SimpleDBOps - Simplified encrypted database operations
|
||||
*
|
||||
* Linus式简化:
|
||||
* - 删除所有复杂的抽象层
|
||||
* - 直接的CRUD操作
|
||||
* - 自动加密/解密
|
||||
* - 没有特殊情况处理
|
||||
* Linus-style simplification:
|
||||
* - Remove all complex abstraction layers
|
||||
* - Direct CRUD operations
|
||||
* - Automatic encryption/decryption
|
||||
* - No special case handling
|
||||
*/
|
||||
class SimpleDBOps {
|
||||
/**
|
||||
* 插入加密记录
|
||||
* Insert encrypted record
|
||||
*/
|
||||
static async insert<T extends Record<string, any>>(
|
||||
table: SQLiteTable<any>,
|
||||
@@ -24,18 +24,18 @@ class SimpleDBOps {
|
||||
data: T,
|
||||
userId: string,
|
||||
): Promise<T> {
|
||||
// 验证用户访问权限
|
||||
// Verify user access permissions
|
||||
if (!DataCrypto.canUserAccessData(userId)) {
|
||||
throw new Error(`User ${userId} data not unlocked`);
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
// Encrypt data
|
||||
const encryptedData = DataCrypto.encryptRecordForUser(tableName, data, userId);
|
||||
|
||||
// 插入数据库
|
||||
// Insert into database
|
||||
const result = await db.insert(table).values(encryptedData).returning();
|
||||
|
||||
// 解密返回结果
|
||||
// Decrypt return result
|
||||
const decryptedResult = DataCrypto.decryptRecordForUser(
|
||||
tableName,
|
||||
result[0],
|
||||
@@ -53,22 +53,22 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询多条记录
|
||||
* Query multiple records
|
||||
*/
|
||||
static async select<T extends Record<string, any>>(
|
||||
query: any,
|
||||
tableName: TableName,
|
||||
userId: string,
|
||||
): Promise<T[]> {
|
||||
// 验证用户访问权限
|
||||
// Verify user access permissions
|
||||
if (!DataCrypto.canUserAccessData(userId)) {
|
||||
throw new Error(`User ${userId} data not unlocked`);
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
// Execute query
|
||||
const results = await query;
|
||||
|
||||
// 解密结果
|
||||
// Decrypt results
|
||||
const decryptedResults = DataCrypto.decryptRecordsForUser(
|
||||
tableName,
|
||||
results,
|
||||
@@ -86,23 +86,23 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条记录
|
||||
* Query single record
|
||||
*/
|
||||
static async selectOne<T extends Record<string, any>>(
|
||||
query: any,
|
||||
tableName: TableName,
|
||||
userId: string,
|
||||
): Promise<T | undefined> {
|
||||
// 验证用户访问权限
|
||||
// Verify user access permissions
|
||||
if (!DataCrypto.canUserAccessData(userId)) {
|
||||
throw new Error(`User ${userId} data not unlocked`);
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
// Execute query
|
||||
const result = await query;
|
||||
if (!result) return undefined;
|
||||
|
||||
// 解密结果
|
||||
// Decrypt results
|
||||
const decryptedResult = DataCrypto.decryptRecordForUser(
|
||||
tableName,
|
||||
result,
|
||||
@@ -120,7 +120,7 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
* Update record
|
||||
*/
|
||||
static async update<T extends Record<string, any>>(
|
||||
table: SQLiteTable<any>,
|
||||
@@ -129,22 +129,22 @@ class SimpleDBOps {
|
||||
data: Partial<T>,
|
||||
userId: string,
|
||||
): Promise<T[]> {
|
||||
// 验证用户访问权限
|
||||
// Verify user access permissions
|
||||
if (!DataCrypto.canUserAccessData(userId)) {
|
||||
throw new Error(`User ${userId} data not unlocked`);
|
||||
}
|
||||
|
||||
// 加密更新数据
|
||||
// Encrypt update data
|
||||
const encryptedData = DataCrypto.encryptRecordForUser(tableName, data, userId);
|
||||
|
||||
// 执行更新
|
||||
// Execute update
|
||||
const result = await db
|
||||
.update(table)
|
||||
.set(encryptedData)
|
||||
.where(where)
|
||||
.returning();
|
||||
|
||||
// 解密返回数据
|
||||
// Decrypt return data
|
||||
const decryptedResults = DataCrypto.decryptRecordsForUser(
|
||||
tableName,
|
||||
result,
|
||||
@@ -162,7 +162,7 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记录
|
||||
* Delete record
|
||||
*/
|
||||
static async delete(
|
||||
table: SQLiteTable<any>,
|
||||
@@ -183,18 +183,18 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* Health check
|
||||
*/
|
||||
static async healthCheck(userId: string): Promise<boolean> {
|
||||
return DataCrypto.canUserAccessData(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 特殊方法:返回加密数据(用于自动启动等场景)
|
||||
* 不解密,直接返回加密状态的数据
|
||||
* Special method: return encrypted data (for auto-start scenarios)
|
||||
* No decryption, return data in encrypted state directly
|
||||
*/
|
||||
static async selectEncrypted(query: any, tableName: TableName): Promise<any[]> {
|
||||
// 直接执行查询,不进行解密
|
||||
// Execute query directly, no decryption
|
||||
const results = await query;
|
||||
|
||||
databaseLogger.debug(`Selected ${results.length} encrypted records from ${tableName}`, {
|
||||
|
||||
@@ -7,19 +7,19 @@ import { eq } from "drizzle-orm";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
/**
|
||||
* SystemCrypto - 开源友好的JWT密钥管理
|
||||
* SystemCrypto - Open source friendly JWT key management
|
||||
*
|
||||
* Linus原则:
|
||||
* - 删除复杂的"系统主密钥"层 - 不解决真实威胁
|
||||
* - 删除硬编码默认密钥 - 开源软件的安全灾难
|
||||
* - 首次启动自动生成 - 每个实例独立安全
|
||||
* - 简单直接,专注真正的安全边界
|
||||
* Linus principles:
|
||||
* - Remove complex "system master key" layer - doesn't solve real threats
|
||||
* - Remove hardcoded default keys - security disaster for open source software
|
||||
* - Auto-generate on first startup - each instance independently secure
|
||||
* - Simple and direct, focus on real security boundaries
|
||||
*/
|
||||
class SystemCrypto {
|
||||
private static instance: SystemCrypto;
|
||||
private jwtSecret: string | null = null;
|
||||
|
||||
// 存储路径配置
|
||||
// Storage path configuration
|
||||
private static readonly JWT_SECRET_FILE = path.join(process.cwd(), '.termix', 'jwt.key');
|
||||
private static readonly JWT_SECRET_DB_KEY = 'system_jwt_secret';
|
||||
|
||||
@@ -33,7 +33,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化JWT密钥 - 开源友好的方式
|
||||
* Initialize JWT secret - open source friendly way
|
||||
*/
|
||||
async initializeJWTSecret(): Promise<void> {
|
||||
try {
|
||||
@@ -41,7 +41,7 @@ class SystemCrypto {
|
||||
operation: "jwt_init",
|
||||
});
|
||||
|
||||
// 1. 环境变量优先(生产环境最佳实践)
|
||||
// 1. Environment variable priority (production best practice)
|
||||
const envSecret = process.env.JWT_SECRET;
|
||||
if (envSecret && envSecret.length >= 64) {
|
||||
this.jwtSecret = envSecret;
|
||||
@@ -52,7 +52,7 @@ class SystemCrypto {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查文件系统存储
|
||||
// 2. Check filesystem storage
|
||||
const fileSecret = await this.loadSecretFromFile();
|
||||
if (fileSecret) {
|
||||
this.jwtSecret = fileSecret;
|
||||
@@ -63,7 +63,7 @@ class SystemCrypto {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 检查数据库存储
|
||||
// 3. Check database storage
|
||||
const dbSecret = await this.loadSecretFromDB();
|
||||
if (dbSecret) {
|
||||
this.jwtSecret = dbSecret;
|
||||
@@ -74,7 +74,7 @@ class SystemCrypto {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 生成新密钥并持久化
|
||||
// 4. Generate new key and persist
|
||||
await this.generateAndStoreSecret();
|
||||
|
||||
} catch (error) {
|
||||
@@ -86,7 +86,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JWT密钥
|
||||
* Get JWT secret
|
||||
*/
|
||||
async getJWTSecret(): Promise<string> {
|
||||
if (!this.jwtSecret) {
|
||||
@@ -96,7 +96,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新密钥并持久化存储
|
||||
* Generate new key and persist storage
|
||||
*/
|
||||
private async generateAndStoreSecret(): Promise<void> {
|
||||
const newSecret = crypto.randomBytes(32).toString('hex');
|
||||
@@ -107,7 +107,7 @@ class SystemCrypto {
|
||||
instanceId
|
||||
});
|
||||
|
||||
// 尝试文件存储(优先,因为更快且不依赖数据库)
|
||||
// Try file storage (priority, faster and doesn't depend on database)
|
||||
try {
|
||||
await this.saveSecretToFile(newSecret);
|
||||
databaseLogger.info("✅ JWT secret saved to file", {
|
||||
@@ -120,7 +120,7 @@ class SystemCrypto {
|
||||
error: fileError instanceof Error ? fileError.message : "Unknown error"
|
||||
});
|
||||
|
||||
// 文件存储失败,使用数据库
|
||||
// File storage failed, use database
|
||||
await this.saveSecretToDB(newSecret, instanceId);
|
||||
databaseLogger.info("✅ JWT secret saved to database", {
|
||||
operation: "jwt_db_saved"
|
||||
@@ -136,21 +136,21 @@ class SystemCrypto {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 文件存储方法 =====
|
||||
// ===== File storage methods =====
|
||||
|
||||
/**
|
||||
* 保存密钥到文件
|
||||
* Save key to file
|
||||
*/
|
||||
private async saveSecretToFile(secret: string): Promise<void> {
|
||||
const dir = path.dirname(SystemCrypto.JWT_SECRET_FILE);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(SystemCrypto.JWT_SECRET_FILE, secret, {
|
||||
mode: 0o600 // 只有owner可读写
|
||||
mode: 0o600 // Only owner can read/write
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载密钥
|
||||
* Load key from file
|
||||
*/
|
||||
private async loadSecretFromFile(): Promise<string | null> {
|
||||
try {
|
||||
@@ -163,15 +163,15 @@ class SystemCrypto {
|
||||
length: secret.length
|
||||
});
|
||||
} catch (error) {
|
||||
// 文件不存在或无法读取,这是正常的
|
||||
// File doesn't exist or can't be read, this is normal
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 数据库存储方法 =====
|
||||
// ===== Database storage methods =====
|
||||
|
||||
/**
|
||||
* 保存密钥到数据库(明文存储,不假装加密有用)
|
||||
* Save key to database (plaintext storage, don't pretend encryption helps)
|
||||
*/
|
||||
private async saveSecretToDB(secret: string, instanceId: string): Promise<void> {
|
||||
const secretData = {
|
||||
@@ -202,7 +202,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库加载密钥
|
||||
* Load key from database
|
||||
*/
|
||||
private async loadSecretFromDB(): Promise<string | null> {
|
||||
try {
|
||||
@@ -217,7 +217,7 @@ class SystemCrypto {
|
||||
|
||||
const secretData = JSON.parse(result[0].value);
|
||||
|
||||
// 检查密钥有效性
|
||||
// Check key validity
|
||||
if (!secretData.secret || secretData.secret.length < 64) {
|
||||
databaseLogger.warn("Invalid JWT secret in database", {
|
||||
operation: "jwt_db_invalid",
|
||||
@@ -238,7 +238,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成JWT密钥(管理功能)
|
||||
* Regenerate JWT secret (admin function)
|
||||
*/
|
||||
async regenerateJWTSecret(): Promise<string> {
|
||||
databaseLogger.warn("🔄 Regenerating JWT secret - ALL TOKENS WILL BE INVALIDATED", {
|
||||
@@ -256,7 +256,7 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT密钥系统
|
||||
* Validate JWT secret system
|
||||
*/
|
||||
async validateJWTSecret(): Promise<boolean> {
|
||||
try {
|
||||
@@ -265,7 +265,7 @@ class SystemCrypto {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 测试JWT操作
|
||||
// Test JWT operations
|
||||
const jwt = await import("jsonwebtoken");
|
||||
const testPayload = { test: true, timestamp: Date.now() };
|
||||
const token = jwt.default.sign(testPayload, secret, { expiresIn: "1s" });
|
||||
@@ -281,22 +281,22 @@ class SystemCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JWT密钥状态(简化版本)
|
||||
* Get JWT key status (simplified version)
|
||||
*/
|
||||
async getSystemKeyStatus() {
|
||||
const isValid = await this.validateJWTSecret();
|
||||
const hasSecret = this.jwtSecret !== null;
|
||||
|
||||
// 检查文件存储
|
||||
// Check file storage
|
||||
let hasFileStorage = false;
|
||||
try {
|
||||
await fs.access(SystemCrypto.JWT_SECRET_FILE);
|
||||
hasFileStorage = true;
|
||||
} catch {
|
||||
// 文件不存在
|
||||
// File doesn't exist
|
||||
}
|
||||
|
||||
// 检查数据库存储
|
||||
// Check database storage
|
||||
let hasDBStorage = false;
|
||||
let dbInfo = null;
|
||||
try {
|
||||
@@ -315,10 +315,10 @@ class SystemCrypto {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// 数据库读取失败
|
||||
// Database read failed
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
// Check environment variable
|
||||
const hasEnvVar = !!(process.env.JWT_SECRET && process.env.JWT_SECRET.length >= 64);
|
||||
|
||||
return {
|
||||
|
||||
@@ -20,36 +20,36 @@ interface EncryptedDEK {
|
||||
}
|
||||
|
||||
interface UserSession {
|
||||
dataKey: Buffer; // 直接存储DEK,删除just-in-time幻想
|
||||
dataKey: Buffer; // Store DEK directly, delete just-in-time fantasy
|
||||
lastActivity: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserCrypto - 简单直接的用户加密
|
||||
* UserCrypto - Simple direct user encryption
|
||||
*
|
||||
* Linus原则:
|
||||
* - 删除just-in-time幻想,直接缓存DEK
|
||||
* - 合理的2小时超时,不是5分钟的用户体验灾难
|
||||
* - 简单可工作的实现,不是理论上完美的垃圾
|
||||
* - 服务器重启后session失效(这是合理的)
|
||||
* Linus principles:
|
||||
* - Delete just-in-time fantasy, cache DEK directly
|
||||
* - Reasonable 2-hour timeout, not 5-minute user experience disaster
|
||||
* - Simple working implementation, not theoretically perfect garbage
|
||||
* - Server restart invalidates sessions (this is reasonable)
|
||||
*/
|
||||
class UserCrypto {
|
||||
private static instance: UserCrypto;
|
||||
private userSessions: Map<string, UserSession> = new Map();
|
||||
|
||||
// 配置常量 - 合理的超时设置
|
||||
// Configuration constants - reasonable timeout settings
|
||||
private static readonly PBKDF2_ITERATIONS = 100000;
|
||||
private static readonly KEK_LENGTH = 32;
|
||||
private static readonly DEK_LENGTH = 32;
|
||||
private static readonly SESSION_DURATION = 2 * 60 * 60 * 1000; // 2小时,合理的用户体验
|
||||
private static readonly MAX_INACTIVITY = 30 * 60 * 1000; // 30分钟,不是1分钟的灾难
|
||||
private static readonly SESSION_DURATION = 2 * 60 * 60 * 1000; // 2 hours, reasonable user experience
|
||||
private static readonly MAX_INACTIVITY = 30 * 60 * 1000; // 30 minutes, not 1-minute disaster
|
||||
|
||||
private constructor() {
|
||||
// 合理的清理间隔
|
||||
// Reasonable cleanup interval
|
||||
setInterval(() => {
|
||||
this.cleanupExpiredSessions();
|
||||
}, 5 * 60 * 1000); // 每5分钟清理一次,不是30秒
|
||||
}, 5 * 60 * 1000); // Clean every 5 minutes, not 30 seconds
|
||||
}
|
||||
|
||||
static getInstance(): UserCrypto {
|
||||
@@ -60,7 +60,7 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册:生成KEK salt和DEK
|
||||
* User registration: generate KEK salt and DEK
|
||||
*/
|
||||
async setupUserEncryption(userId: string, password: string): Promise<void> {
|
||||
const kekSalt = await this.generateKEKSalt();
|
||||
@@ -71,7 +71,7 @@ class UserCrypto {
|
||||
const encryptedDEK = this.encryptDEK(DEK, KEK);
|
||||
await this.storeEncryptedDEK(userId, encryptedDEK);
|
||||
|
||||
// 立即清理临时密钥
|
||||
// Immediately clean temporary keys
|
||||
KEK.fill(0);
|
||||
DEK.fill(0);
|
||||
|
||||
@@ -82,12 +82,12 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户认证:验证密码并缓存DEK
|
||||
* 删除了just-in-time幻想,直接工作
|
||||
* User authentication: validate password and cache DEK
|
||||
* Deleted just-in-time fantasy, works directly
|
||||
*/
|
||||
async authenticateUser(userId: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
// 验证密码并解密DEK
|
||||
// Validate password and decrypt DEK
|
||||
const kekSalt = await this.getKEKSalt(userId);
|
||||
if (!kekSalt) return false;
|
||||
|
||||
@@ -99,24 +99,24 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
const DEK = this.decryptDEK(encryptedDEK, KEK);
|
||||
KEK.fill(0); // 立即清理KEK
|
||||
KEK.fill(0); // Immediately clean KEK
|
||||
|
||||
// 创建用户会话,直接缓存DEK
|
||||
// Create user session, cache DEK directly
|
||||
const now = Date.now();
|
||||
|
||||
// 清理旧会话
|
||||
// Clean old session
|
||||
const oldSession = this.userSessions.get(userId);
|
||||
if (oldSession) {
|
||||
oldSession.dataKey.fill(0);
|
||||
}
|
||||
|
||||
this.userSessions.set(userId, {
|
||||
dataKey: Buffer.from(DEK), // 复制DEK
|
||||
dataKey: Buffer.from(DEK), // Copy DEK
|
||||
lastActivity: now,
|
||||
expiresAt: now + UserCrypto.SESSION_DURATION,
|
||||
});
|
||||
|
||||
DEK.fill(0); // 清理临时DEK
|
||||
DEK.fill(0); // Clean temporary DEK
|
||||
|
||||
databaseLogger.success("User authenticated and DEK cached", {
|
||||
operation: "user_crypto_auth",
|
||||
@@ -136,8 +136,8 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据密钥 - 简单直接从缓存返回
|
||||
* 删除了just-in-time推导垃圾
|
||||
* Get user data key - simple direct return from cache
|
||||
* Deleted just-in-time derivation garbage
|
||||
*/
|
||||
getUserDataKey(userId: string): Buffer | null {
|
||||
const session = this.userSessions.get(userId);
|
||||
@@ -147,7 +147,7 @@ class UserCrypto {
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 检查会话是否过期
|
||||
// Check if session has expired
|
||||
if (now > session.expiresAt) {
|
||||
this.userSessions.delete(userId);
|
||||
session.dataKey.fill(0);
|
||||
@@ -158,7 +158,7 @@ class UserCrypto {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否超过最大不活跃时间
|
||||
// Check if max inactivity time exceeded
|
||||
if (now - session.lastActivity > UserCrypto.MAX_INACTIVITY) {
|
||||
this.userSessions.delete(userId);
|
||||
session.dataKey.fill(0);
|
||||
@@ -169,19 +169,19 @@ class UserCrypto {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新最后活动时间
|
||||
// Update last activity time
|
||||
session.lastActivity = now;
|
||||
return session.dataKey;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户登出:清理会话
|
||||
* User logout: clear session
|
||||
*/
|
||||
logoutUser(userId: string): void {
|
||||
const session = this.userSessions.get(userId);
|
||||
if (session) {
|
||||
session.dataKey.fill(0); // 安全清理密钥
|
||||
session.dataKey.fill(0); // Securely clear key
|
||||
this.userSessions.delete(userId);
|
||||
}
|
||||
databaseLogger.info("User logged out", {
|
||||
@@ -191,22 +191,22 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已解锁
|
||||
* Check if user is unlocked
|
||||
*/
|
||||
isUserUnlocked(userId: string): boolean {
|
||||
return this.getUserDataKey(userId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户密码
|
||||
* Change user password
|
||||
*/
|
||||
async changeUserPassword(userId: string, oldPassword: string, newPassword: string): Promise<boolean> {
|
||||
try {
|
||||
// 验证旧密码
|
||||
// Validate old password
|
||||
const isValid = await this.validatePassword(userId, oldPassword);
|
||||
if (!isValid) return false;
|
||||
|
||||
// 获取当前DEK
|
||||
// Get current DEK
|
||||
const kekSalt = await this.getKEKSalt(userId);
|
||||
if (!kekSalt) return false;
|
||||
|
||||
@@ -216,21 +216,21 @@ class UserCrypto {
|
||||
|
||||
const DEK = this.decryptDEK(encryptedDEK, oldKEK);
|
||||
|
||||
// 生成新的KEK salt和加密DEK
|
||||
// Generate new KEK salt and encrypt DEK
|
||||
const newKekSalt = await this.generateKEKSalt();
|
||||
const newKEK = this.deriveKEK(newPassword, newKekSalt);
|
||||
const newEncryptedDEK = this.encryptDEK(DEK, newKEK);
|
||||
|
||||
// 存储新的salt和encrypted DEK
|
||||
// Store new salt and encrypted DEK
|
||||
await this.storeKEKSalt(userId, newKekSalt);
|
||||
await this.storeEncryptedDEK(userId, newEncryptedDEK);
|
||||
|
||||
// 清理所有临时密钥
|
||||
// Clean all temporary keys
|
||||
oldKEK.fill(0);
|
||||
newKEK.fill(0);
|
||||
DEK.fill(0);
|
||||
|
||||
// 清理用户会话,要求重新登录
|
||||
// Clean user session, require re-login
|
||||
this.logoutUser(userId);
|
||||
|
||||
return true;
|
||||
@@ -239,7 +239,7 @@ class UserCrypto {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
// ===== Private methods =====
|
||||
|
||||
private async validatePassword(userId: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -252,7 +252,7 @@ class UserCrypto {
|
||||
|
||||
const DEK = this.decryptDEK(encryptedDEK, KEK);
|
||||
|
||||
// 清理临时密钥
|
||||
// Clean temporary keys
|
||||
KEK.fill(0);
|
||||
DEK.fill(0);
|
||||
|
||||
@@ -268,7 +268,7 @@ class UserCrypto {
|
||||
|
||||
for (const [userId, session] of this.userSessions.entries()) {
|
||||
if (now > session.expiresAt || now - session.lastActivity > UserCrypto.MAX_INACTIVITY) {
|
||||
session.dataKey.fill(0); // 安全清理密钥
|
||||
session.dataKey.fill(0); // Securely clear key
|
||||
expiredUsers.push(userId);
|
||||
}
|
||||
}
|
||||
@@ -285,7 +285,7 @@ class UserCrypto {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 数据库操作和加密方法(简化版本) =====
|
||||
// ===== Database operations and encryption methods (simplified version) =====
|
||||
|
||||
private async generateKEKSalt(): Promise<KEKSalt> {
|
||||
return {
|
||||
@@ -337,7 +337,7 @@ class UserCrypto {
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// 数据库操作方法
|
||||
// Database operation methods
|
||||
private async storeKEKSalt(userId: string, kekSalt: KEKSalt): Promise<void> {
|
||||
const key = `user_kek_salt_${userId}`;
|
||||
const value = JSON.stringify(kekSalt);
|
||||
|
||||
@@ -28,19 +28,19 @@ interface UserExportData {
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDataExport - 用户级数据导入导出
|
||||
* UserDataExport - User-level data import/export
|
||||
*
|
||||
* Linus原则:
|
||||
* - 用户拥有自己的数据,应该能自由导出
|
||||
* - 简单直接,没有复杂的权限检查
|
||||
* - 支持加密和明文两种格式
|
||||
* - 不破坏现有系统架构
|
||||
* Linus principles:
|
||||
* - Users own their data and should be able to export freely
|
||||
* - Simple and direct, no complex permission checks
|
||||
* - Support both encrypted and plaintext formats
|
||||
* - Don't break existing system architecture
|
||||
*/
|
||||
class UserDataExport {
|
||||
private static readonly EXPORT_VERSION = "v2.0";
|
||||
|
||||
/**
|
||||
* 导出用户数据
|
||||
* Export user data
|
||||
*/
|
||||
static async exportUserData(
|
||||
userId: string,
|
||||
@@ -61,7 +61,7 @@ class UserDataExport {
|
||||
includeCredentials,
|
||||
});
|
||||
|
||||
// 验证用户存在
|
||||
// Verify user exists
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0) {
|
||||
throw new Error(`User not found: ${userId}`);
|
||||
@@ -69,7 +69,7 @@ class UserDataExport {
|
||||
|
||||
const userRecord = user[0];
|
||||
|
||||
// 获取用户数据密钥(如果需要解密)
|
||||
// Get user data key (if decryption needed)
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (format === 'plaintext') {
|
||||
userDataKey = DataCrypto.getUserDataKey(userId);
|
||||
@@ -78,13 +78,13 @@ class UserDataExport {
|
||||
}
|
||||
}
|
||||
|
||||
// 导出SSH主机配置
|
||||
// Export SSH host configurations
|
||||
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凭据(如果包含)
|
||||
// Export SSH credentials (if included)
|
||||
let sshCredentialsData: any[] = [];
|
||||
if (includeCredentials) {
|
||||
const credentials = await db.select().from(sshCredentials).where(eq(sshCredentials.userId, userId));
|
||||
@@ -93,17 +93,17 @@ class UserDataExport {
|
||||
: credentials;
|
||||
}
|
||||
|
||||
// 导出文件管理器数据
|
||||
// Export file manager data
|
||||
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)),
|
||||
]);
|
||||
|
||||
// 导出已忽略的警告
|
||||
// Export dismissed alerts
|
||||
const alerts = await db.select().from(dismissedAlerts).where(eq(dismissedAlerts.userId, userId));
|
||||
|
||||
// 构建导出数据
|
||||
// Build export data
|
||||
const exportData: UserExportData = {
|
||||
version: this.EXPORT_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
@@ -148,7 +148,7 @@ class UserDataExport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为JSON字符串
|
||||
* Export as JSON string
|
||||
*/
|
||||
static async exportUserDataToJSON(
|
||||
userId: string,
|
||||
@@ -165,7 +165,7 @@ class UserDataExport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证导出数据格式
|
||||
* Validate export data format
|
||||
*/
|
||||
static validateExportData(data: any): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
@@ -191,7 +191,7 @@ class UserDataExport {
|
||||
errors.push("Missing or invalid metadata field");
|
||||
}
|
||||
|
||||
// 检查必需的数据字段
|
||||
// Check required data fields
|
||||
if (data.userData) {
|
||||
const requiredFields = ['sshHosts', 'sshCredentials', 'fileManagerData', 'dismissedAlerts'];
|
||||
for (const field of requiredFields) {
|
||||
@@ -214,7 +214,7 @@ class UserDataExport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导出数据统计信息
|
||||
* Get export data statistics
|
||||
*/
|
||||
static getExportStats(data: UserExportData): {
|
||||
version: string;
|
||||
|
||||
@@ -27,18 +27,18 @@ interface ImportResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDataImport - 用户数据导入
|
||||
* UserDataImport - User data import
|
||||
*
|
||||
* Linus原则:
|
||||
* - 导入不应该破坏现有数据(除非明确要求)
|
||||
* - 支持dry-run模式验证
|
||||
* - 处理ID冲突的简单策略:重新生成
|
||||
* - 错误处理要明确,不能静默失败
|
||||
* Linus principles:
|
||||
* - Import should not break existing data (unless explicitly requested)
|
||||
* - Support dry-run mode for validation
|
||||
* - Simple strategy for ID conflicts: regenerate
|
||||
* - Error handling must be explicit, no silent failures
|
||||
*/
|
||||
class UserDataImport {
|
||||
|
||||
/**
|
||||
* 导入用户数据
|
||||
* Import user data
|
||||
*/
|
||||
static async importUserData(
|
||||
targetUserId: string,
|
||||
@@ -64,19 +64,19 @@ class UserDataImport {
|
||||
skipFileManagerData,
|
||||
});
|
||||
|
||||
// 验证目标用户存在
|
||||
// Verify target user exists
|
||||
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}`);
|
||||
}
|
||||
|
||||
// 验证导出数据格式
|
||||
// Validate export data format
|
||||
const validation = UserDataExport.validateExportData(exportData);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid export data: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// 验证用户数据已解锁(如果数据是加密的)
|
||||
// Verify user data is unlocked (if data is encrypted)
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (exportData.metadata.encrypted) {
|
||||
userDataKey = DataCrypto.getUserDataKey(targetUserId);
|
||||
@@ -98,7 +98,7 @@ class UserDataImport {
|
||||
dryRun,
|
||||
};
|
||||
|
||||
// 导入SSH主机配置
|
||||
// Import SSH host configurations
|
||||
if (exportData.userData.sshHosts && exportData.userData.sshHosts.length > 0) {
|
||||
const importStats = await this.importSshHosts(
|
||||
targetUserId,
|
||||
@@ -110,7 +110,7 @@ class UserDataImport {
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入SSH凭据
|
||||
// Import SSH credentials
|
||||
if (!skipCredentials && exportData.userData.sshCredentials && exportData.userData.sshCredentials.length > 0) {
|
||||
const importStats = await this.importSshCredentials(
|
||||
targetUserId,
|
||||
@@ -122,7 +122,7 @@ class UserDataImport {
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入文件管理器数据
|
||||
// Import file manager data
|
||||
if (!skipFileManagerData && exportData.userData.fileManagerData) {
|
||||
const importStats = await this.importFileManagerData(
|
||||
targetUserId,
|
||||
@@ -134,7 +134,7 @@ class UserDataImport {
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
// 导入忽略的警告
|
||||
// Import dismissed alerts
|
||||
if (exportData.userData.dismissedAlerts && exportData.userData.dismissedAlerts.length > 0) {
|
||||
const importStats = await this.importDismissedAlerts(
|
||||
targetUserId,
|
||||
@@ -167,7 +167,7 @@ class UserDataImport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入SSH主机配置
|
||||
* Import SSH host configurations
|
||||
*/
|
||||
private static async importSshHosts(
|
||||
targetUserId: string,
|
||||
@@ -185,16 +185,16 @@ class UserDataImport {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新生成ID避免冲突
|
||||
// Regenerate ID to avoid conflicts
|
||||
const newHostData = {
|
||||
...host,
|
||||
id: undefined, // 让数据库自动生成
|
||||
id: undefined, // Let database auto-generate
|
||||
userId: targetUserId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 如果数据需要重新加密
|
||||
// If data needs re-encryption
|
||||
let processedHostData = newHostData;
|
||||
if (options.userDataKey) {
|
||||
processedHostData = DataCrypto.encryptRecord("ssh_data", newHostData, targetUserId, options.userDataKey);
|
||||
@@ -212,7 +212,7 @@ class UserDataImport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入SSH凭据
|
||||
* Import SSH credentials
|
||||
*/
|
||||
private static async importSshCredentials(
|
||||
targetUserId: string,
|
||||
@@ -230,18 +230,18 @@ class UserDataImport {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新生成ID避免冲突
|
||||
// Regenerate ID to avoid conflicts
|
||||
const newCredentialData = {
|
||||
...credential,
|
||||
id: undefined, // 让数据库自动生成
|
||||
id: undefined, // Let database auto-generate
|
||||
userId: targetUserId,
|
||||
usageCount: 0, // 重置使用计数
|
||||
usageCount: 0, // Reset usage count
|
||||
lastUsed: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 如果数据需要重新加密
|
||||
// If data needs re-encryption
|
||||
let processedCredentialData = newCredentialData;
|
||||
if (options.userDataKey) {
|
||||
processedCredentialData = DataCrypto.encryptRecord("ssh_credentials", newCredentialData, targetUserId, options.userDataKey);
|
||||
@@ -259,7 +259,7 @@ class UserDataImport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入文件管理器数据
|
||||
* Import file manager data
|
||||
*/
|
||||
private static async importFileManagerData(
|
||||
targetUserId: string,
|
||||
@@ -271,7 +271,7 @@ class UserDataImport {
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
// 导入最近文件
|
||||
// Import recent files
|
||||
if (fileManagerData.recent && Array.isArray(fileManagerData.recent)) {
|
||||
for (const item of fileManagerData.recent) {
|
||||
try {
|
||||
@@ -292,7 +292,7 @@ class UserDataImport {
|
||||
}
|
||||
}
|
||||
|
||||
// 导入固定文件
|
||||
// Import pinned files
|
||||
if (fileManagerData.pinned && Array.isArray(fileManagerData.pinned)) {
|
||||
for (const item of fileManagerData.pinned) {
|
||||
try {
|
||||
@@ -313,7 +313,7 @@ class UserDataImport {
|
||||
}
|
||||
}
|
||||
|
||||
// 导入快捷方式
|
||||
// Import shortcuts
|
||||
if (fileManagerData.shortcuts && Array.isArray(fileManagerData.shortcuts)) {
|
||||
for (const item of fileManagerData.shortcuts) {
|
||||
try {
|
||||
@@ -341,7 +341,7 @@ class UserDataImport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入忽略的警告
|
||||
* Import dismissed alerts
|
||||
*/
|
||||
private static async importDismissedAlerts(
|
||||
targetUserId: string,
|
||||
@@ -359,7 +359,7 @@ class UserDataImport {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的警告
|
||||
// Check if alert already exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(dismissedAlerts)
|
||||
@@ -402,7 +402,7 @@ class UserDataImport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON字符串导入
|
||||
* Import from JSON string
|
||||
*/
|
||||
static async importUserDataFromJSON(
|
||||
targetUserId: string,
|
||||
|
||||
Reference in New Issue
Block a user