You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

117 lines
3.7 KiB

/**
* 部署后首次启动前执行:若库中尚无 admin,则用环境变量创建首个管理员。
* 逻辑对齐 `packages/drizzle-pkg/seed.ts`,使用 SQLite(与 `migrate-sqlite.js` 相同)。
*
* 环境变量(与 seed.ts 一致):
* - DATABASE_URL:SQLite 路径,可为 `file:/path/to/db.sqlite` 或裸路径
* - BOOTSTRAP_ADMIN_USERNAME / BOOTSTRAP_ADMIN_PASSWORD:可选;未设置或校验失败则跳过
*/
import Database from 'better-sqlite3'
import { hash } from 'bcryptjs'
import { mkdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
/** 与 `server/service/auth/index.ts` 一致 */
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
const MIN_PASSWORD_LENGTH = 6
/** 与 seed.ts 一致:仅当小写用户名本身符合 slug 规则时写入 public_slug */
const PUBLIC_SLUG_REGEX = /^[a-z0-9]{3,20}$/
function derivePublicSlug(username) {
const lower = username.toLowerCase()
return PUBLIC_SLUG_REGEX.test(lower) ? lower : null
}
/**
* 与 `migrate-sqlite.js` 一致:相对路径相对 `process.cwd()`(例如 `.output/run.sh` 下与迁移写入同一文件)。
* Nitro 内 `resolveSqliteDatabaseUrl` 在无法锚定 drizzle-pkg 时也会回退到 cwd,避免「迁移/seed 成功、服务 CANTOPEN」。
*/
function resolveSqliteFilePath(dbUrl) {
const stripped = dbUrl.startsWith('file:') ? dbUrl.slice('file:'.length) : dbUrl
if (!stripped) {
throw new Error('DATABASE_URL 未设置,且未提供有效的 SQLite 文件路径')
}
if (path.isAbsolute(stripped)) {
return stripped
}
return path.resolve(process.cwd(), stripped)
}
function openSqlite() {
const dbUrl = process.env.DATABASE_URL || ''
const sqlitePath = resolveSqliteFilePath(dbUrl)
mkdirSync(path.dirname(sqlitePath), { recursive: true })
return new Database(sqlitePath)
}
async function main() {
const db = openSqlite()
try {
const existingAdmin = db
.prepare(`SELECT id FROM users WHERE role = ? LIMIT 1`)
.get('admin')
if (existingAdmin) {
console.log('Bootstrap skipped: admin exists')
return
}
const username = process.env.BOOTSTRAP_ADMIN_USERNAME
const password = process.env.BOOTSTRAP_ADMIN_PASSWORD
if (!username || !password) {
console.warn(
'Bootstrap skipped: set BOOTSTRAP_ADMIN_USERNAME and BOOTSTRAP_ADMIN_PASSWORD',
)
return
}
if (!USERNAME_REGEX.test(username) || password.length < MIN_PASSWORD_LENGTH) {
console.warn(
'Bootstrap skipped: invalid BOOTSTRAP_ADMIN_USERNAME or BOOTSTRAP_ADMIN_PASSWORD',
)
return
}
const passwordHash = await hash(password, 10)
const { maxId } = db.prepare(`SELECT COALESCE(MAX(id), 0) AS maxId FROM users`).get()
const userId = (maxId ?? 0) + 1
const publicSlug = derivePublicSlug(username)
try {
db.prepare(
`
INSERT INTO users (id, username, password, role, status, public_slug)
VALUES (@id, @username, @password, @role, @status, @public_slug)
`,
).run({
id: userId,
username,
password: passwordHash,
role: 'admin',
status: 'active',
public_slug: publicSlug,
})
console.log('Bootstrap complete: admin user created')
} catch (err) {
console.warn(
'Bootstrap skipped: could not insert admin (unique conflict or DB error)',
err,
)
}
} finally {
db.close()
}
}
const isMain =
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
if (isMain) {
// 成功时自然退出(退出码 0);仅未捕获错误时 process.exit(1)。
main().catch((e) => {
console.error(e)
process.exit(1)
})
}