feat: v0.3.0 版本 - SSH终端、SFTP、端口转发功能实现
核心功能: - SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮 - SFTP管理: 双栏文件管理、上传下载、传输队列 - 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理 架构改进: - 拆分大型组件 (SFTPView/TabBar/Sidebar) - 添加统一日志和错误处理 - 修复React严格模式下的终端黑屏问题 UI更新: - 新增端口转发视图 - 更新UI预览文档 - 左侧栏右键菜单支持SFTP快捷打开
This commit is contained in:
1 parent
cf1c054381
commit
484d850abd
105 files changed
+25330
-64
No files matched your search
@@ -0,0 +1,310 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { simpleEncrypt, simpleDecrypt } from './crypto'
|
||||
import { keyService } from './key-service'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
const logger = createLogger('DatabaseService')
|
||||
|
||||
export class DatabaseService {
|
||||
private db: Database.Database | null = null
|
||||
private dbPath: string
|
||||
private masterKey: string | null = null
|
||||
|
||||
constructor() {
|
||||
// 使用应用数据目录存储数据库
|
||||
const userDataPath = app.getPath('userData')
|
||||
this.dbPath = path.join(userDataPath, 'opsxshell.db')
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
try {
|
||||
// 确保目录存在
|
||||
const dbDir = path.dirname(this.dbPath)
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 获取主密钥
|
||||
this.masterKey = keyService.getMasterKey()
|
||||
|
||||
this.db = new Database(this.dbPath)
|
||||
this.createTables()
|
||||
logger.info('Database initialized at:', this.dbPath)
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private createTables(): void {
|
||||
if (!this.db) return
|
||||
|
||||
// 会话表
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER DEFAULT 22,
|
||||
username TEXT NOT NULL,
|
||||
auth_type TEXT CHECK(auth_type IN ('password', 'key', 'agent')),
|
||||
password TEXT,
|
||||
private_key_path TEXT,
|
||||
passphrase_encrypted TEXT,
|
||||
group_name TEXT DEFAULT 'default',
|
||||
tags TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// 迁移:如果 password 列不存在,添加它
|
||||
try {
|
||||
this.db.exec(`ALTER TABLE sessions ADD COLUMN password TEXT`)
|
||||
} catch (e) {
|
||||
// 列可能已存在,忽略错误
|
||||
}
|
||||
|
||||
// 连接历史表
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS connection_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT REFERENCES sessions(id),
|
||||
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
disconnected_at DATETIME,
|
||||
duration INTEGER,
|
||||
status TEXT
|
||||
)
|
||||
`)
|
||||
|
||||
// 设置表
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// 密钥表
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT CHECK(type IN ('rsa', 'ed25519', 'ecdsa')),
|
||||
private_key_encrypted TEXT NOT NULL,
|
||||
public_key TEXT,
|
||||
passphrase_encrypted TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// 端口转发规则表
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS port_forwards (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT REFERENCES sessions(id),
|
||||
type TEXT CHECK(type IN ('local', 'remote', 'dynamic')),
|
||||
local_host TEXT,
|
||||
local_port INTEGER,
|
||||
remote_host TEXT,
|
||||
remote_port INTEGER,
|
||||
is_active BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
// 加密字段
|
||||
private encryptField(value: string | null | undefined): string | null {
|
||||
if (!value) return null
|
||||
if (!this.masterKey) throw new Error('Master key not initialized')
|
||||
return simpleEncrypt(value, this.masterKey)
|
||||
}
|
||||
|
||||
// 解密字段
|
||||
private decryptField(value: string | null | undefined): string | null {
|
||||
if (!value) return null
|
||||
if (!this.masterKey) {
|
||||
logger.error('Decrypt failed: Master key not initialized')
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const decrypted = simpleDecrypt(value, this.masterKey)
|
||||
logger.info('Decrypted field length:', decrypted?.length || 0)
|
||||
return decrypted
|
||||
} catch (error) {
|
||||
logger.error('Decrypt failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 会话 CRUD 操作
|
||||
createSession(session: any): any {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
logger.info('Creating session with password:', session.password ? `len=${session.password.length}` : 'null/undefined')
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sessions (id, name, host, port, username, auth_type, password, private_key_path, passphrase_encrypted, group_name, tags)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
const id = `session-${Date.now()}`
|
||||
const encryptedPassword = this.encryptField(session.password)
|
||||
logger.info('Encrypted password:', encryptedPassword ? `len=${encryptedPassword.length}` : 'null')
|
||||
|
||||
stmt.run(
|
||||
id,
|
||||
session.name,
|
||||
session.host,
|
||||
session.port || 22,
|
||||
session.username,
|
||||
session.authType,
|
||||
encryptedPassword,
|
||||
session.privateKey || null, // 私钥路径不加密
|
||||
this.encryptField(session.passphrase),
|
||||
session.group || 'default',
|
||||
session.tags ? JSON.stringify(session.tags) : null
|
||||
)
|
||||
|
||||
return this.getSessionById(id)
|
||||
}
|
||||
|
||||
getAllSessions(): any[] {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM sessions ORDER BY created_at DESC')
|
||||
const sessions = stmt.all()
|
||||
|
||||
return sessions.map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
host: s.host,
|
||||
port: s.port,
|
||||
username: s.username,
|
||||
authType: s.auth_type,
|
||||
password: this.decryptField(s.password),
|
||||
privateKey: s.private_key_path,
|
||||
passphrase: this.decryptField(s.passphrase_encrypted),
|
||||
group: s.group_name,
|
||||
tags: s.tags ? JSON.parse(s.tags) : [],
|
||||
}))
|
||||
}
|
||||
|
||||
getSessionById(id: string): any {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM sessions WHERE id = ?')
|
||||
const session = stmt.get(id) as any
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
host: session.host,
|
||||
port: session.port,
|
||||
username: session.username,
|
||||
authType: session.auth_type,
|
||||
password: this.decryptField(session.password),
|
||||
privateKey: session.private_key_path,
|
||||
passphrase: this.decryptField(session.passphrase_encrypted),
|
||||
group: session.group_name,
|
||||
tags: session.tags ? JSON.parse(session.tags) : [],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
updateSession(id: string, updates: any): any {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
fields.push('name = ?')
|
||||
values.push(updates.name)
|
||||
}
|
||||
if (updates.host !== undefined) {
|
||||
fields.push('host = ?')
|
||||
values.push(updates.host)
|
||||
}
|
||||
if (updates.port !== undefined) {
|
||||
fields.push('port = ?')
|
||||
values.push(updates.port)
|
||||
}
|
||||
if (updates.username !== undefined) {
|
||||
fields.push('username = ?')
|
||||
values.push(updates.username)
|
||||
}
|
||||
if (updates.authType !== undefined) {
|
||||
fields.push('auth_type = ?')
|
||||
values.push(updates.authType)
|
||||
}
|
||||
if (updates.password !== undefined) {
|
||||
fields.push('password = ?')
|
||||
values.push(this.encryptField(updates.password))
|
||||
}
|
||||
if (updates.passphrase !== undefined) {
|
||||
fields.push('passphrase_encrypted = ?')
|
||||
values.push(this.encryptField(updates.passphrase))
|
||||
}
|
||||
if (updates.group !== undefined) {
|
||||
fields.push('group_name = ?')
|
||||
values.push(updates.group)
|
||||
}
|
||||
if (updates.tags !== undefined) {
|
||||
fields.push('tags = ?')
|
||||
values.push(JSON.stringify(updates.tags))
|
||||
}
|
||||
|
||||
fields.push('updated_at = CURRENT_TIMESTAMP')
|
||||
values.push(id)
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sessions SET ${fields.join(', ')} WHERE id = ?
|
||||
`)
|
||||
stmt.run(...values)
|
||||
|
||||
return this.getSessionById(id)
|
||||
}
|
||||
|
||||
deleteSession(id: string): void {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM sessions WHERE id = ?')
|
||||
stmt.run(id)
|
||||
}
|
||||
|
||||
// 设置操作
|
||||
getSetting(key: string): any {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const stmt = this.db.prepare('SELECT value FROM settings WHERE key = ?')
|
||||
const row: any = stmt.get(key)
|
||||
return row ? JSON.parse(row.value) : null
|
||||
}
|
||||
|
||||
setSetting(key: string, value: any): void {
|
||||
if (!this.db) throw new Error('Database not initialized')
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO settings (key, value, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`)
|
||||
stmt.run(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.db) {
|
||||
this.db.close()
|
||||
this.db = null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user