169 lines
7.4 KiB
TypeScript
169 lines
7.4 KiB
TypeScript
// src/lib/monitor/settings-manager.ts
|
||
import { getDb } from '@/lib/db'
|
||
import crypto from 'crypto'
|
||
import type { MonitorConfig, WebhookConfig } from './types'
|
||
|
||
const ALGORITHM = 'aes-256-cbc'
|
||
|
||
function getEncryptionKey(): string {
|
||
const db = getDb()
|
||
const row = db.prepare("SELECT value FROM settings WHERE key = 'monitor.encryption_key'").get() as { value: string } | undefined
|
||
if (row) return row.value
|
||
// 首次启动:生成密钥
|
||
const newKey = crypto.randomBytes(32).toString('hex')
|
||
db.prepare("INSERT OR IGNORE INTO settings (key, value, category) VALUES ('monitor.encryption_key', ?, 'security')").run(newKey)
|
||
return newKey
|
||
}
|
||
|
||
export function encryptValue(plaintext: string): string {
|
||
if (!plaintext) return ''
|
||
const key = Buffer.from(getEncryptionKey(), 'hex')
|
||
const iv = crypto.randomBytes(16)
|
||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
||
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
|
||
encrypted += cipher.final('hex')
|
||
return iv.toString('hex') + ':' + encrypted
|
||
}
|
||
|
||
export function decryptValue(ciphertext: string): string {
|
||
if (!ciphertext) return ''
|
||
try {
|
||
const key = Buffer.from(getEncryptionKey(), 'hex')
|
||
const [ivHex, encrypted] = ciphertext.split(':')
|
||
const iv = Buffer.from(ivHex, 'hex')
|
||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
||
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
||
decrypted += decipher.final('utf8')
|
||
return decrypted
|
||
} catch {
|
||
console.error('[Settings] 密码解密失败,可能密钥不匹配')
|
||
return ''
|
||
}
|
||
}
|
||
|
||
export function getSetting(key: string): string | null {
|
||
const db = getDb()
|
||
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined
|
||
return row?.value ?? null
|
||
}
|
||
|
||
export function setSetting(key: string, value: string, category = 'general'): void {
|
||
const db = getDb()
|
||
db.prepare(
|
||
"INSERT INTO settings (key, value, category, updated_at) VALUES (?, ?, ?, datetime('now', '+8 hours')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
|
||
).run(key, value, category)
|
||
}
|
||
|
||
const DEFAULT_WEBHOOK: WebhookConfig = {
|
||
id: 'default',
|
||
name: '默认推送',
|
||
url: '',
|
||
enabled: true,
|
||
params: { interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 },
|
||
}
|
||
|
||
const DEFAULT_CONFIG: MonitorConfig = {
|
||
monitor: { enabled: false, interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 },
|
||
mail: { address: 'gxp@qx002575.com', imap_server: 'imaphz.qiye.163.com', imap_port: 993, pop3_server: 'pophz.qiye.163.com', pop3_port: 995, smtp_server: 'smtphz.qiye.163.com', smtp_port: 465, password: '' },
|
||
filter: {
|
||
subject_keywords: ['服务器故障单'],
|
||
sender_email: 'tencent_dcops@tencent.com',
|
||
oem_repair_keywords: { base_keyword: '服务器故障单', type_keyword: 'OEM维修', exclude_keyword: 'OEM诊断' },
|
||
oem_diag_keywords: { base_keyword: '服务器故障单', type_keyword: 'OEM诊断', exclude_keyword: 'OEM维修' },
|
||
},
|
||
wechat: { webhooks: [JSON.parse(JSON.stringify(DEFAULT_WEBHOOK))] },
|
||
}
|
||
|
||
export function generateWebhookId(): string {
|
||
return 'wh_' + crypto.randomBytes(8).toString('hex')
|
||
}
|
||
|
||
// 将旧的 wechat.webhook_url 迁移为 wechat.webhooks 数组
|
||
function migrateWebhookSettings(db: ReturnType<typeof getDb>): void {
|
||
const legacyUrl = db.prepare("SELECT value FROM settings WHERE key = 'wechat.webhook_url'").get() as { value: string } | undefined
|
||
if (legacyUrl?.value) {
|
||
const webhook: WebhookConfig = {
|
||
id: generateWebhookId(),
|
||
name: '默认推送',
|
||
url: legacyUrl.value,
|
||
enabled: true,
|
||
params: { interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 },
|
||
}
|
||
setSetting('wechat.webhooks', JSON.stringify([webhook]), 'wechat')
|
||
db.prepare("DELETE FROM settings WHERE key = 'wechat.webhook_url'").run()
|
||
}
|
||
}
|
||
|
||
export function getMonitorConfig(): MonitorConfig {
|
||
const config = JSON.parse(JSON.stringify(DEFAULT_CONFIG))
|
||
const db = getDb()
|
||
|
||
// 迁移旧格式
|
||
migrateWebhookSettings(db)
|
||
|
||
const mappings: Record<string, (v: string) => void> = {
|
||
'monitor.enabled': (v) => { config.monitor.enabled = v === 'true' },
|
||
'monitor.interval_seconds': (v) => { config.monitor.interval_seconds = parseInt(v) || 60 },
|
||
'monitor.push_delay_ms': (v) => { config.monitor.push_delay_ms = parseInt(v) || 2000 },
|
||
'monitor.silent_start_hour': (v) => { config.monitor.silent_start_hour = parseInt(v) || 0 },
|
||
'monitor.silent_end_hour': (v) => { config.monitor.silent_end_hour = parseInt(v) || 7 },
|
||
'mail.address': (v) => { config.mail.address = v },
|
||
'mail.imap_server': (v) => { config.mail.imap_server = v },
|
||
'mail.imap_port': (v) => { config.mail.imap_port = parseInt(v) || 993 },
|
||
'mail.pop3_server': (v) => { config.mail.pop3_server = v },
|
||
'mail.pop3_port': (v) => { config.mail.pop3_port = parseInt(v) || 995 },
|
||
'mail.smtp_server': (v) => { config.mail.smtp_server = v },
|
||
'mail.smtp_port': (v) => { config.mail.smtp_port = parseInt(v) || 465 },
|
||
'mail.password': (v) => { config.mail.password = v ? decryptValue(v) : '' },
|
||
'filter.subject_keywords': (v) => { try { config.filter.subject_keywords = JSON.parse(v) } catch {} },
|
||
'filter.sender_email': (v) => { config.filter.sender_email = v },
|
||
'filter.oem_repair_keywords': (v) => { try { config.filter.oem_repair_keywords = JSON.parse(v) } catch {} },
|
||
'filter.oem_diag_keywords': (v) => { try { config.filter.oem_diag_keywords = JSON.parse(v) } catch {} },
|
||
'wechat.webhooks': (v) => {
|
||
try {
|
||
const parsed = JSON.parse(v)
|
||
if (Array.isArray(parsed) && parsed.length > 0) config.wechat.webhooks = parsed
|
||
} catch {}
|
||
},
|
||
}
|
||
const rows = db.prepare("SELECT key, value FROM settings WHERE key LIKE 'monitor.%' OR key LIKE 'mail.%' OR key LIKE 'filter.%' OR key LIKE 'wechat.%'").all() as { key: string; value: string }[]
|
||
for (const row of rows) {
|
||
mappings[row.key]?.(row.value)
|
||
}
|
||
return config
|
||
}
|
||
|
||
export function updateMonitorConfig(updates: Record<string, unknown>): void {
|
||
const encryptedFields = new Set(['mail.password'])
|
||
for (const [key, value] of Object.entries(updates)) {
|
||
if (key === 'monitor.encryption_key') continue // 不允许修改密钥
|
||
let stringValue: string
|
||
if (typeof value === 'boolean') stringValue = String(value)
|
||
else if (typeof value === 'number') stringValue = String(value)
|
||
else if (typeof value === 'object') stringValue = JSON.stringify(value)
|
||
else stringValue = String(value ?? '')
|
||
if (encryptedFields.has(key) && stringValue && !stringValue.match(/^[0-9a-f]{32}:/)) {
|
||
stringValue = encryptValue(stringValue)
|
||
}
|
||
const category = key.split('.')[0]
|
||
setSetting(key, stringValue, category)
|
||
}
|
||
}
|
||
|
||
export function getMaskedConfig(): MonitorConfig {
|
||
const config = getMonitorConfig()
|
||
if (config.mail.password) config.mail.password = '••••••••'
|
||
// 深拷贝 webhooks 数组,避免 mask 操作污染 getMonitorConfig() 返回的原对象
|
||
// (前端拿到 masked URL 后若不重新输入就测试,会用 •••••••• 调企业微信导致 93000 invalid url)
|
||
config.wechat.webhooks = config.wechat.webhooks.map(wh => {
|
||
if (wh.url) {
|
||
const keyMatch = wh.url.match(/key=([0-9a-f-]+)/)
|
||
if (keyMatch) {
|
||
return { ...wh, url: wh.url.replace(keyMatch[1], '••••••••') }
|
||
}
|
||
}
|
||
return wh
|
||
})
|
||
return config
|
||
}
|