import { stdin as input, stdout as output } from "node:process"; import { mkdirSync } from "node:fs"; import { randomUUID } from "node:crypto"; import { openDatabase } from "../db/index.js"; import { acquireInstanceLock, loadConfig, prepareDataDirectories } from "../config.js"; import { hashPassword, normalizeUsername, validateNewPassword, temporaryPassword } from "../security.js"; import { writeAudit } from "../audit.js"; function arg(name: string): string | undefined { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } async function readSecret(prompt: string): Promise { if (!input.isTTY) throw new Error("admin:init 需要交互式 TTY,不能通过管道传入密码"); output.write(prompt); return await new Promise((resolve, reject) => { let value = ""; const wasRaw = Boolean(input.isRaw); const onData = (chunk: Buffer) => { const text = chunk.toString("utf8"); if (text === "\u0003") { cleanup(); reject(new Error("已取消")); } else if (text === "\r" || text === "\n") { cleanup(); output.write("\n"); resolve(value); } else if (text === "\u007f") { value = value.slice(0, -1); } else if (!text.includes("\u001b")) { value += text; } }; const cleanup = () => { input.off("data", onData); input.setRawMode?.(wasRaw); input.pause(); }; input.resume(); input.setRawMode?.(true); input.on("data", onData); }); } async function main() { const config = loadConfig(); prepareDataDirectories(config); mkdirSync(config.dataDir, { recursive: true, mode: 0o700 }); const release = acquireInstanceLock(config); const database = openDatabase(config); try { const existing = database.sqlite.prepare("SELECT COUNT(*) AS count FROM admins").get() as { count: number }; if (existing.count > 0) throw new Error("INITIAL_ADMIN_EXISTS:管理员已经初始化"); const username = arg("--username") ?? (await readSecret("用户名: ")); const displayName = arg("--display-name") ?? (await readSecret("显示名称: ")); const generate = process.argv.includes("--generate"); let password = generate ? temporaryPassword() : await readSecret("密码(至少 12 个字符): "); if (!generate) { const confirmation = await readSecret("再次输入密码: "); if (password !== confirmation) throw new Error("两次密码输入不一致"); } const policyError = validateNewPassword(password); if (policyError) throw new Error(policyError); const normalized = normalizeUsername(username); if ([...normalized].length < 3) throw new Error("用户名至少需要 3 个字符"); const passwordHash = await hashPassword(password); const id = randomUUID(); const now = Date.now(); database.sqlite.transaction(() => { const current = database.sqlite.prepare("SELECT COUNT(*) AS count FROM admins").get() as { count: number }; if (current.count > 0) throw new Error("INITIAL_ADMIN_EXISTS:管理员已经初始化"); database.sqlite.prepare(` INSERT INTO admins(id, username, username_norm, display_name, password_hash, status, must_change_password, auth_version, version, created_at) VALUES (?, ?, ?, ?, ?, 'active', 1, 1, 1, ?) `).run(id, username.normalize("NFKC").trim(), normalized, displayName.trim(), passwordHash, now); writeAudit(database.sqlite, { requestId: `cli:${randomUUID()}`, actorUsername: "cli", action: "admin.initialized", targetType: "admin", targetId: id, after: { username: normalized, displayName: displayName.trim(), status: "active" }, }); })(); console.log(generate ? `已创建首位管理员。一次性密码:${password}` : "已创建首位管理员。"); } finally { database.sqlite.close(); release(); } } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });