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.
118 lines
3.6 KiB
118 lines
3.6 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) {
|
|
main()
|
|
.then(() => process.exit(0))
|
|
.catch((e) => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|
|
}
|
|
|