voko-dev-user.js 3.43 KB
#!/usr/bin/env node
import fs from 'node:fs'
import { randomBytes, randomUUID, scryptSync } from 'node:crypto'
import path from 'node:path'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { devMysqlUrl } from '@drama/db'

const DEFAULT_DATABASE_URL = devMysqlUrl('voko')
const here = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(here, '../..')
const envFile = path.resolve(repoRoot, 'apps/voko-server/.env')
const requireFromVokoServer = createRequire(new URL('../../apps/voko-server/package.json', import.meta.url))
const { createDb } = await import(requireFromVokoServer.resolve('@drama/db'))

function hashPassword(password) {
  const salt = randomBytes(16)
  const derived = scryptSync(String(password), salt, 64)
  return `scrypt$${salt.toString('hex')}$${derived.toString('hex')}`
}

async function main() {
  const email = process.argv[2] || 'admin@example.test'
  const password = process.argv[3] || 'admin123'
  const displayName = process.argv[4] || 'VOKO Admin'
  const role = process.argv[5] || 'admin'
  if (!['admin', 'operator', 'user'].includes(role)) {
    throw new Error(`voko dev user role 必须是 admin、operator 或 user: ${role}`)
  }
  const databaseUrl = process.env.DATABASE_URL || readDotenv(envFile).DATABASE_URL || DEFAULT_DATABASE_URL
  const db = createDb({ url: databaseUrl, pool: { min: 0, max: 1 } })
  const normalizedEmail = email.trim().toLowerCase()
  const now = new Date()
  const user = {
    id: randomUUID(),
    email: normalizedEmail,
    display_name: displayName,
    status: 'active',
    password_hash: hashPassword(password),
    locale: 'zh',
    email_verified_at: now,
    role,
    created_at: now,
    updated_at: now,
  }

  try {
    await db('users').insert(user).onConflict('email').merge({
      display_name: user.display_name,
      status: user.status,
      password_hash: user.password_hash,
      locale: user.locale,
      email_verified_at: db.raw('COALESCE(email_verified_at, ?)', [now]),
      role: user.role,
      updated_at: now,
    })
    // 主人 id(Agent 全名「名字@主人 id」的后半段):取邮箱 @ 前的部分,已有则保留。
    const stored = await db('users').where({ email: normalizedEmail }).first('id')
    const handle = normalizedEmail.split('@')[0].replace(/[^a-z0-9_-]/g, '-').slice(0, 32)
    await db('vk_owner_handles').insert({ user_id: stored.id, handle, created_at: now }).onConflict('user_id').ignore()
    const saved = await db('vk_owner_handles').where({ user_id: stored.id }).first('handle')
    if (saved === undefined) throw new Error(`voko dev user: id ${handle} 已被别的账号占用,请换一个邮箱前缀`)
    console.log(`voko dev user ready: ${normalizedEmail}, role=${role}, id=${saved.handle}`)
  } finally {
    await db.destroy()
  }
}

function readDotenv(file) {
  if (!fs.existsSync(file)) return {}
  const env = {}
  for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
    const line = raw.trim()
    if (!line || line.startsWith('#')) continue
    const separator = line.indexOf('=')
    if (separator < 0) continue
    const key = line.slice(0, separator).trim()
    let value = line.slice(separator + 1).trim()
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1)
    }
    env[key] = value
  }
  return env
}

main().catch(error => {
  console.error(error)
  process.exit(1)
})