import crypto from 'node:crypto'; const TABLE = 'h5_admin_system_test_accounts'; function resolveEncryptionKey(explicitKey) { const raw = explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret'; return crypto.createHash('sha256').update(raw).digest(); } function encryptSecret(plaintext, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), }; } function decryptSecret({ ciphertext, iv, tag }, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64')); decipher.setAuthTag(Buffer.from(tag, 'base64')); const plain = Buffer.concat([ decipher.update(Buffer.from(ciphertext, 'base64')), decipher.final(), ]); return plain.toString('utf8'); } function maskApiKey(apiKey) { if (!apiKey) return ''; if (apiKey.length <= 8) return '*'.repeat(apiKey.length); const head = apiKey.slice(0, 4); const tail = apiKey.slice(-4); return `${head}${'*'.repeat(Math.max(apiKey.length - 8, 4))}${tail}`; } function normalizeText(value) { return String(value ?? '').trim(); } function toAccountRow(row) { return { id: row.id, label: row.label, username: row.username, passwordMasked: maskApiKey(row.password_preview ?? ''), createdAt: Number(row.created_at ?? 0) || 0, updatedAt: Number(row.updated_at ?? 0) || 0, updatedBy: row.updated_by ?? null, }; } export async function ensureSystemTestAccountSchema(pool) { await pool.query(` CREATE TABLE IF NOT EXISTS ${TABLE} ( id CHAR(36) NOT NULL PRIMARY KEY, label VARCHAR(120) NOT NULL, username VARCHAR(120) NOT NULL, password_ciphertext TEXT NOT NULL, password_iv VARCHAR(255) NOT NULL, password_tag VARCHAR(255) NOT NULL, password_preview VARCHAR(255) NOT NULL DEFAULT '', updated_by CHAR(36) NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uniq_${TABLE}_username (username) ) `); } export function createSystemTestAccountService(pool) { if (!pool) { throw new Error('system test account service requires pool'); } async function listAccounts() { const [rows] = await pool.query( `SELECT id, label, username, password_preview, updated_by, created_at, updated_at FROM ${TABLE} ORDER BY updated_at DESC, created_at DESC`, ); return rows.map(toAccountRow); } async function getAccountSecret(accountId) { const [rows] = await pool.query( `SELECT id, label, username, password_ciphertext, password_iv, password_tag FROM ${TABLE} WHERE id = ? LIMIT 1`, [accountId], ); const row = rows[0] ?? null; if (!row) return null; return { id: row.id, label: row.label, username: row.username, password: decryptSecret({ ciphertext: row.password_ciphertext, iv: row.password_iv, tag: row.password_tag, }), }; } async function createAccount({ label, username, password, updatedBy }) { const normalizedUsername = normalizeText(username); const normalizedLabel = normalizeText(label) || normalizedUsername; if (!normalizedUsername || !password) { return { ok: false, message: '账号和密码不能为空' }; } const id = crypto.randomUUID(); const now = Date.now(); const encrypted = encryptSecret(String(password)); try { await pool.query( `INSERT INTO ${TABLE} (id, label, username, password_ciphertext, password_iv, password_tag, password_preview, updated_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, normalizedLabel, normalizedUsername, encrypted.ciphertext, encrypted.iv, encrypted.tag, maskApiKey(String(password)), updatedBy ?? null, now, now, ], ); } catch (error) { if (error?.code === 'ER_DUP_ENTRY') { return { ok: false, message: '该测试账号已存在,请直接使用或先删除旧账号' }; } throw error; } const account = await getAccountById(id); return { ok: true, account }; } async function getAccountById(accountId) { const [rows] = await pool.query( `SELECT id, label, username, password_preview, updated_by, created_at, updated_at FROM ${TABLE} WHERE id = ? LIMIT 1`, [accountId], ); const row = rows[0] ?? null; return row ? toAccountRow(row) : null; } async function deleteAccount(accountId) { const existing = await getAccountById(accountId); if (!existing) return { ok: false, message: '测试账号不存在' }; await pool.query(`DELETE FROM ${TABLE} WHERE id = ? LIMIT 1`, [accountId]); return { ok: true }; } return { listAccounts, getAccountSecret, createAccount, deleteAccount, }; }