187 lines
4.4 KiB
TypeScript
187 lines
4.4 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
|
|
import {
|
|
generateUsernameFromEmail,
|
|
hashPassword,
|
|
validateUsername,
|
|
} from '@hexahost/auth';
|
|
|
|
const PLATFORM_ROLES = [
|
|
'USER',
|
|
'SUPPORT',
|
|
'MODERATOR',
|
|
'ADMIN',
|
|
'SUPER_ADMIN',
|
|
] as const;
|
|
|
|
interface CliArgs {
|
|
email: string;
|
|
password: string;
|
|
username?: string;
|
|
displayName?: string;
|
|
}
|
|
|
|
function parseArgs(argv: string[]): CliArgs {
|
|
const args: Record<string, string> = {};
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const key = argv[i];
|
|
if (key === '--') {
|
|
continue;
|
|
}
|
|
const value = argv[i + 1];
|
|
if (key?.startsWith('--') && value) {
|
|
args[key.slice(2)] = value;
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
if (!args['email'] || !args['password']) {
|
|
console.error(
|
|
'Usage: bootstrap-admin --email <email> --password <password> [--username <name>] [--display-name <name>]',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (args['password'].length < 12) {
|
|
console.error('Password must be at least 12 characters.');
|
|
process.exit(1);
|
|
}
|
|
|
|
return {
|
|
email: args['email'].trim().toLowerCase(),
|
|
password: args['password'],
|
|
username: args['username'],
|
|
displayName: args['display-name'],
|
|
};
|
|
}
|
|
|
|
async function ensurePlatformRoles(prisma: PrismaClient): Promise<void> {
|
|
for (const name of PLATFORM_ROLES) {
|
|
await prisma.platformRole.upsert({
|
|
where: { name },
|
|
create: { name, description: `${name} platform role` },
|
|
update: {},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function ensureUniqueUsername(
|
|
prisma: PrismaClient,
|
|
baseUsername: string,
|
|
): Promise<string> {
|
|
let candidate = baseUsername;
|
|
let suffix = 0;
|
|
|
|
while (true) {
|
|
const existing = await prisma.user.findUnique({
|
|
where: { username: candidate },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!existing) {
|
|
return candidate;
|
|
}
|
|
|
|
suffix += 1;
|
|
const suffixText = String(suffix);
|
|
candidate = `${baseUsername.slice(0, Math.max(1, 32 - suffixText.length))}${suffixText}`;
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const input = parseArgs(process.argv.slice(2));
|
|
const prisma = new PrismaClient();
|
|
|
|
try {
|
|
await ensurePlatformRoles(prisma);
|
|
|
|
const existing = await prisma.user.findUnique({
|
|
where: { email: input.email },
|
|
});
|
|
|
|
if (existing) {
|
|
const superAdminRole = await prisma.platformRole.findUnique({
|
|
where: { name: 'SUPER_ADMIN' },
|
|
});
|
|
|
|
if (!superAdminRole) {
|
|
throw new Error('SUPER_ADMIN role missing after seed');
|
|
}
|
|
|
|
await prisma.userPlatformRole.upsert({
|
|
where: {
|
|
userId_roleId: {
|
|
userId: existing.id,
|
|
roleId: superAdminRole.id,
|
|
},
|
|
},
|
|
create: {
|
|
userId: existing.id,
|
|
roleId: superAdminRole.id,
|
|
},
|
|
update: {},
|
|
});
|
|
|
|
console.log(`User ${input.email} already exists — SUPER_ADMIN role ensured.`);
|
|
return;
|
|
}
|
|
|
|
let username = input.username?.trim().toLowerCase();
|
|
if (username) {
|
|
const validation = validateUsername(username);
|
|
if (!validation.valid) {
|
|
throw new Error(validation.error ?? 'Invalid username');
|
|
}
|
|
} else {
|
|
username = generateUsernameFromEmail(input.email);
|
|
}
|
|
|
|
username = await ensureUniqueUsername(prisma, username);
|
|
|
|
const passwordHash = await hashPassword(input.password);
|
|
|
|
const superAdminRole = await prisma.platformRole.findUniqueOrThrow({
|
|
where: { name: 'SUPER_ADMIN' },
|
|
});
|
|
|
|
const user = await prisma.$transaction(async (tx) => {
|
|
const created = await tx.user.create({
|
|
data: {
|
|
email: input.email,
|
|
username,
|
|
displayName: input.displayName ?? 'Administrator',
|
|
emailVerifiedAt: new Date(),
|
|
credentials: {
|
|
create: { passwordHash },
|
|
},
|
|
platformRoles: {
|
|
create: { roleId: superAdminRole.id },
|
|
},
|
|
},
|
|
});
|
|
|
|
await tx.auditEvent.create({
|
|
data: {
|
|
userId: created.id,
|
|
action: 'admin.bootstrap',
|
|
entityType: 'user',
|
|
entityId: created.id,
|
|
metadata: { source: 'bootstrap-cli' },
|
|
},
|
|
});
|
|
|
|
return created;
|
|
});
|
|
|
|
console.log(`Admin user created: ${user.email} (${user.id})`);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
}
|
|
|
|
void main().catch((error: unknown) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|