voko-dev-user.js
3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/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)
})