import "dotenv/config"; import { eq } from "drizzle-orm"; import { db } from "../db"; import { user } from "../db/schema"; import { auth } from "../lib/auth"; function requireEnv(name: string): string { const value = process.env[name]?.trim(); if (!value) throw new Error(`${name} is not set in .env`); return value; } async function seedAdmin() { // The username plugin stores usernames lowercased, so lookups must match that. const displayUsername = requireEnv("SEED_ADMIN_USERNAME"); const username = displayUsername.toLowerCase(); const password = requireEnv("SEED_ADMIN_PASSWORD"); const name = process.env.SEED_ADMIN_NAME?.trim() || "Administrator"; const email = process.env.SEED_ADMIN_EMAIL?.trim() || `${username}@cleo.local`; const [existing] = await db .select({ id: user.id }) .from(user) .where(eq(user.username, username)) .limit(1); if (existing) { console.log(`Admin "${username}" already exists, skipped.`); return; } // Goes through Better Auth's internal adapter instead of the sign-up endpoint, which is // closed by `disableSignUp`. Hashing with ctx.password keeps the format login expects. const ctx = await auth.$context; const created = await ctx.internalAdapter.createUser( { name, email, username, displayUsername }, { method: "admin" }, ); await ctx.internalAdapter.linkAccount({ userId: created.id, providerId: "credential", accountId: created.id, password: await ctx.password.hash(password), }); console.log(`Admin "${username}" created.`); } seedAdmin() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(() => db.$client.end());