/** * 部署后首次启动前执行:若库中尚无 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) }) }