import { existsSync, readFileSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { randomBytes, scryptSync } from "node:crypto"; import { createClient } from "@supabase/supabase-js"; loadEnvFile(".env"); loadEnvFile(".env.local"); const args = parseArgs(process.argv.slice(2)); const phone = normalizePhone(args.phone || process.env.ZHINIAN_BOOTSTRAP_ADMIN_PHONE || ""); const password = args.password || process.env.ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD || ""; const displayName = args.name || process.env.ZHINIAN_BOOTSTRAP_ADMIN_NAME || "平台超级管理员"; if (!/^\+?[0-9]{6,20}$/.test(phone)) fail("请通过 --phone 或 ZHINIAN_BOOTSTRAP_ADMIN_PHONE 提供有效手机号。"); if (password.length < 8) fail("请通过 --password 或 ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD 提供至少 8 位密码。"); const credential = hashPassword(password); const now = new Date().toISOString(); const supabase = getSupabase(); if (supabase) { const { data: existing, error: lookupError } = await supabase.from("platform_users").select("id, role, password_hash").eq("role", "super_admin").limit(1).maybeSingle(); if (lookupError) fail(lookupError.message); const { data: phoneOwner, error: phoneLookupError } = await supabase.from("platform_users").select("id").eq("phone", phone).limit(1).maybeSingle(); if (phoneLookupError) fail(phoneLookupError.message); if (phoneOwner && phoneOwner.id !== existing?.id) fail("该手机号已经绑定其他账号,不能初始化为超级管理员。"); if (existing && existing.password_hash) fail("平台已经存在超级管理员,初始化已停止。"); if (existing) { const { error } = await supabase.from("platform_users").update({ phone, display_name: displayName, password_hash: credential.hash, password_salt: credential.salt, status: "active", failed_login_count: 0, locked_until: null, session_version: 1, updated_at: now }).eq("id", existing.id); if (error) fail(error.message); console.log(`已初始化超级管理员:${phone}(${existing.id})`); } else { const user = { id: `user_${randomBytes(8).toString("hex")}`, phone, display_name: displayName, role: "super_admin", organization_id: null, status: "active", password_hash: credential.hash, password_salt: credential.salt, failed_login_count: 0, locked_until: null, session_version: 1, created_at: now, updated_at: now }; const { error } = await supabase.from("platform_users").insert(user); if (error) fail(error.message); console.log(`已初始化超级管理员:${phone}(${user.id})`); } } else { const dataDirectory = process.env.ZHINIAN_DATA_DIR || join(process.cwd(), ".runtime", "data"); await mkdir(dataDirectory, { recursive: true }); const path = join(dataDirectory, "platform-accounts.json"); const state = await readState(path); const existing = state.users.find((user) => user.role === "super_admin"); const phoneOwner = state.users.find((user) => user.phone === phone && user.id !== existing?.id); if (phoneOwner) fail("该手机号已经绑定其他账号,不能初始化为超级管理员。"); if (existing && existing.passwordHash) fail("平台已经存在超级管理员,初始化已停止。"); const user = existing || { id: `user_${randomBytes(8).toString("hex")}`, phone, displayName, role: "super_admin", status: "active", failedLoginCount: 0, sessionVersion: 1, createdAt: now, updatedAt: now }; Object.assign(user, { phone, displayName, passwordHash: credential.hash, passwordSalt: credential.salt, organizationId: undefined, failedLoginCount: 0, lockedUntil: undefined, sessionVersion: 1, updatedAt: now }); if (!existing) state.users.push(user); await writeFile(path, JSON.stringify(state, null, 2)); console.log(`已初始化超级管理员:${phone}(${user.id})`); } function getSupabase() { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; return url && key ? createClient(url, key, { auth: { persistSession: false } }) : null; } function hashPassword(value) { const salt = randomBytes(16).toString("hex"); return { salt, hash: scryptSync(value, salt, 64).toString("hex") }; } function normalizePhone(value) { return value.trim().replace(/[\s()-]/g, ""); } function parseArgs(values) { const result = {}; for (let index = 0; index < values.length; index += 1) { const value = values[index]; if (!value.startsWith("--")) continue; result[value.slice(2)] = values[index + 1] && !values[index + 1].startsWith("--") ? values[++index] : "true"; } return result; } async function readState(path) { if (!existsSync(path)) return { users: [], organizations: [], migrations: [] }; try { return JSON.parse(await readFile(path, "utf8")); } catch { return { users: [], organizations: [], migrations: [] }; } } function loadEnvFile(path) { if (!existsSync(path)) return; const text = requireFile(path); for (const line of text.split(/\r?\n/)) { const match = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)\s*$/); if (!match || process.env[match[1]]) continue; process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, ""); } } function requireFile(path) { return readFileSync(path, "utf8"); } function fail(message) { console.error(`初始化失败:${message}`); process.exit(1); }