Browse Source

feat(db): bootstrap first admin from environment

Made-with: Cursor
feat/multitenant-hub
npmrun 17 hours ago
parent
commit
6860555a8b
  1. 4
      .env.example
  2. 97
      packages/drizzle-pkg/seed.ts

4
.env.example

@ -1 +1,5 @@
DATABASE_URL=postgresql://postgres:xxxxxx@localhost:6666/postgres DATABASE_URL=postgresql://postgres:xxxxxx@localhost:6666/postgres
# Optional: first admin for an empty instance. `bun run db:seed` creates an admin only when no user has role=admin yet (same username/password rules as registration).
BOOTSTRAP_ADMIN_USERNAME=
BOOTSTRAP_ADMIN_PASSWORD=

97
packages/drizzle-pkg/seed.ts

@ -1,25 +1,86 @@
import './env'; import "./env";
import { seed } from "drizzle-seed"; import { hash } from "bcryptjs";
import { usersTable } from "./lib/schema/auth"; import { eq, sql } from "drizzle-orm";
import { dbGlobal } from "./lib/db"; import { dbGlobal } from "./lib/db";
import { users } from "./lib/schema/auth";
/** Match `server/service/auth/index.ts` */
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/;
const MIN_PASSWORD_LENGTH = 6;
/**
* Public slug is optional: use lowercased username only if it satisfies a URL-style slug
* (lowercase alphanumeric only, 320 chars). Usernames with underscores or mixed rules
* that do not fit stay null; the admin can set `publicSlug` later via profile APIs.
*/
const PUBLIC_SLUG_REGEX = /^[a-z0-9]{3,20}$/;
function derivePublicSlug(username: string): string | null {
const lower = username.toLowerCase();
return PUBLIC_SLUG_REGEX.test(lower) ? lower : null;
}
async function getNextUserId() {
const [row] = await dbGlobal
.select({
maxId: sql<number>`COALESCE(MAX(${users.id}), 0)`,
})
.from(users);
return (row?.maxId ?? 0) + 1;
}
async function main() { async function main() {
await seed(dbGlobal, { usersTable }).refine((f) => ({ const [existingAdmin] = await dbGlobal
usersTable: { .select({ id: users.id })
columns: { .from(users)
name: f.fullName(), .where(eq(users.role, "admin"))
age: f.int({ minValue: 18, maxValue: 60 }), .limit(1);
email: f.email(),
}, if (existingAdmin) {
count: 10, console.log("Bootstrap skipped: admin exists");
},
}));
console.log('Seed complete!');
process.exit(0); process.exit(0);
}
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",
);
process.exit(0);
}
if (!USERNAME_REGEX.test(username) || password.length < MIN_PASSWORD_LENGTH) {
console.warn(
"Bootstrap skipped: invalid BOOTSTRAP_ADMIN_USERNAME or BOOTSTRAP_ADMIN_PASSWORD",
);
process.exit(0);
}
const passwordHash = await hash(password, 10);
const userId = await getNextUserId();
const publicSlug = derivePublicSlug(username);
try {
await dbGlobal.insert(users).values({
id: userId,
username,
password: passwordHash,
role: "admin",
status: "active",
publicSlug,
});
console.log("Bootstrap complete: admin user created");
} catch (err) {
console.warn("Bootstrap skipped: could not insert admin (unique conflict or DB error)", err);
process.exit(0);
}
process.exit(0);
} }
main().catch(e => { main().catch((e) => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}); });

Loading…
Cancel
Save