150 lines
3.3 KiB
TypeScript
150 lines
3.3 KiB
TypeScript
import { fileURLToPath } from "node:url"
|
|
import { UserStatus } from "@/generated/prisma/client"
|
|
import { normalizeEmail } from "@/lib/email"
|
|
import { getPasswordHash } from "@/lib/security"
|
|
import prisma from "../src/lib/prisma"
|
|
|
|
type BootstrapAdminInput = {
|
|
email: string
|
|
name: string
|
|
password: string
|
|
}
|
|
|
|
function splitName(name: string) {
|
|
const [firstName = "Administrator", ...rest] = name.trim().split(/\s+/)
|
|
|
|
return {
|
|
firstName,
|
|
lastName: rest.join(" "),
|
|
}
|
|
}
|
|
|
|
function getBootstrapAdminInput(): BootstrapAdminInput {
|
|
const isProduction = process.env.NODE_ENV === "production"
|
|
|
|
const email = process.env.ADMIN_EMAIL ?? "admin@local.host"
|
|
const name = process.env.ADMIN_NAME ?? "Administrator"
|
|
const password = process.env.ADMIN_PASSWORD
|
|
|
|
if (isProduction && !password) {
|
|
throw new Error("ADMIN_PASSWORD is required to bootstrap an admin user")
|
|
}
|
|
|
|
return {
|
|
email,
|
|
name,
|
|
password: password ?? "admin",
|
|
}
|
|
}
|
|
|
|
export async function bootstrapAdmin(client: typeof prisma) {
|
|
const enabled = process.env.ADMIN_BOOTSTRAP_ENABLED !== "false"
|
|
if (!enabled) return
|
|
|
|
const admin = getBootstrapAdminInput()
|
|
const email = normalizeEmail(admin.email)
|
|
const { firstName, lastName } = splitName(admin.name)
|
|
const existingUser = await client.user.findUnique({
|
|
where: {
|
|
emailNormalized: email,
|
|
},
|
|
select: {
|
|
id: true,
|
|
passwordHash: true,
|
|
activatedAt: true,
|
|
person: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
const user = existingUser
|
|
? await client.user.update({
|
|
where: {
|
|
id: existingUser.id,
|
|
},
|
|
data: {
|
|
name: admin.name,
|
|
email: admin.email,
|
|
emailNormalized: email,
|
|
role: "ADMIN",
|
|
status: UserStatus.ACTIVE,
|
|
...(existingUser.passwordHash
|
|
? {}
|
|
: {
|
|
passwordHash: await getPasswordHash(admin.password),
|
|
passwordChangedAt: new Date(),
|
|
}),
|
|
...(existingUser.activatedAt ? {} : { activatedAt: new Date() }),
|
|
},
|
|
select: {
|
|
id: true,
|
|
person: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
: await client.user.create({
|
|
data: {
|
|
name: admin.name,
|
|
email: admin.email,
|
|
emailNormalized: email,
|
|
role: "ADMIN",
|
|
status: UserStatus.ACTIVE,
|
|
passwordHash: await getPasswordHash(admin.password),
|
|
activatedAt: new Date(),
|
|
passwordChangedAt: new Date(),
|
|
},
|
|
select: {
|
|
id: true,
|
|
person: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!user.person) {
|
|
await client.person.upsert({
|
|
where: {
|
|
userId: user.id,
|
|
},
|
|
update: {
|
|
firstName,
|
|
lastName,
|
|
email: admin.email,
|
|
},
|
|
create: {
|
|
firstName,
|
|
lastName,
|
|
email: admin.email,
|
|
user: {
|
|
connect: {
|
|
id: user.id,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
await bootstrapAdmin(prisma)
|
|
} finally {
|
|
await prisma.$disconnect()
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|
|
}
|