fix(bootstrap): include missing bootstrap.service helper
The route handlers in 1a65e02 import hasAnySuperAdmin and
createInitialSuperAdmin from this file; was accidentally left
untracked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
src/lib/services/bootstrap.service.ts
Normal file
82
src/lib/services/bootstrap.service.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* First-run bootstrap: lets the very first operator self-register as the
|
||||
* super-admin on a fresh DB. Safe because the only "do it" path
|
||||
* (`createInitialSuperAdmin`) refuses to run when any super-admin row
|
||||
* already exists, so the window closes the moment the first account is
|
||||
* created.
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { user, userProfiles } from '@/lib/db/schema';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { ConflictError, ValidationError } from '@/lib/errors';
|
||||
|
||||
/** True when no user has `is_super_admin = true` in user_profiles. */
|
||||
export async function hasAnySuperAdmin(): Promise<boolean> {
|
||||
const row = await db
|
||||
.select({ userId: userProfiles.userId })
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.isSuperAdmin, true))
|
||||
.limit(1);
|
||||
return row.length > 0;
|
||||
}
|
||||
|
||||
export interface BootstrapInput {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically: create the better-auth user, create the user_profiles row
|
||||
* with isSuperAdmin=true. Refuses to run when a super-admin already
|
||||
* exists — the only safe-by-design self-registration path.
|
||||
*
|
||||
* Returns the new user's id on success.
|
||||
*/
|
||||
export async function createInitialSuperAdmin(input: BootstrapInput): Promise<string> {
|
||||
if (input.password.length < 9) {
|
||||
throw new ValidationError('Password must be at least 9 characters');
|
||||
}
|
||||
if (!input.email.includes('@')) {
|
||||
throw new ValidationError('A valid email is required');
|
||||
}
|
||||
if (!input.name.trim()) {
|
||||
throw new ValidationError('Name is required');
|
||||
}
|
||||
|
||||
// Re-check inside the critical path so two concurrent first-run
|
||||
// submissions can't both win — the first to insert the profile row
|
||||
// closes the window for everyone else.
|
||||
if (await hasAnySuperAdmin()) {
|
||||
throw new ConflictError('A super-administrator account already exists');
|
||||
}
|
||||
|
||||
const email = input.email.toLowerCase().trim();
|
||||
const existing = await db.query.user.findFirst({ where: eq(user.email, email) });
|
||||
if (existing) {
|
||||
// Either someone signed up via a different flow first, or we're
|
||||
// racing a portal-activation. Refuse rather than silently re-purpose.
|
||||
throw new ConflictError('A user with this email already exists');
|
||||
}
|
||||
|
||||
const authResult = await auth.api.signUpEmail({
|
||||
body: {
|
||||
email,
|
||||
password: input.password,
|
||||
name: input.name.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
const newUserId = authResult.user.id;
|
||||
|
||||
await db.insert(userProfiles).values({
|
||||
userId: newUserId,
|
||||
displayName: input.name.trim(),
|
||||
isSuperAdmin: true,
|
||||
});
|
||||
|
||||
return newUserId;
|
||||
}
|
||||
Reference in New Issue
Block a user