flnxi-Wu/lib/system-init.ts
2026-06-01 17:00:05 +08:00

296 lines
8.2 KiB
TypeScript

import { randomBytes } from 'crypto'
import bcrypt from 'bcryptjs'
import { prisma } from './prisma'
import { BUILTIN_TEMPLATE_KEYS, ensureBuiltinTemplates } from './system-integrity'
const INITIAL_ADMIN_STATE_KEY = 'initial_admin_state'
const DEFAULT_INIT_ADMIN_EMAIL = 'admin@local.flnxi'
type SystemConfigStore = {
systemConfig: {
findUnique: (args: any) => Promise<{ value: string } | null>
upsert: (args: any) => Promise<unknown>
delete: (args: any) => Promise<unknown>
}
}
type PasswordSource = 'environment' | 'generated'
type InitialAdminState = {
userId: string
email: string
mustChangePassword: boolean
createdAt: string
}
export type InitialAdminInfo = {
email: string
username: string
created: boolean
upgraded: boolean
mustChangePassword: boolean
initialPassword?: string
passwordSource?: PasswordSource
}
export type InitializeSystemResult = {
initialized: true
alreadyInitialized: boolean
admin: InitialAdminInfo | null
defaultDepartment: string
defaultCategory: string
defaultScene: string
templates: {
total: number
created: number
createdTemplates: string[]
errors: string[]
}
}
function generateInitialAdminPassword(): string {
return randomBytes(18).toString('base64url')
}
export function getInitAdminEmail(): string {
const configuredEmail = process.env.INIT_ADMIN_EMAIL?.trim()
return configuredEmail || DEFAULT_INIT_ADMIN_EMAIL
}
export function getSystemInitToken(): string {
return process.env.SYSTEM_INIT_TOKEN?.trim() || ''
}
function resolveInitialAdminPassword(): {
password: string
source: PasswordSource
} {
const configuredPassword = process.env.INIT_ADMIN_PASSWORD?.trim()
if (configuredPassword) {
return {
password: configuredPassword,
source: 'environment',
}
}
return {
password: generateInitialAdminPassword(),
source: 'generated',
}
}
async function saveInitialAdminState(userId: string, email: string) {
const payload: InitialAdminState = {
userId,
email,
mustChangePassword: true,
createdAt: new Date().toISOString(),
}
await prisma.systemConfig.upsert({
where: { key: INITIAL_ADMIN_STATE_KEY },
update: { value: JSON.stringify(payload) },
create: {
key: INITIAL_ADMIN_STATE_KEY,
value: JSON.stringify(payload),
},
})
}
export async function getInitialAdminState(): Promise<InitialAdminState | null> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: INITIAL_ADMIN_STATE_KEY },
})
if (!config) {
return null
}
const parsed = JSON.parse(config.value) as InitialAdminState
if (
!parsed ||
typeof parsed.userId !== 'string' ||
typeof parsed.email !== 'string' ||
typeof parsed.mustChangePassword !== 'boolean'
) {
return null
}
return parsed
} catch (error) {
console.warn('[SystemInit] 读取初始管理员状态失败:', error)
return null
}
}
export async function isInitialAdminPasswordChangeRequired(userId: string): Promise<boolean> {
const state = await getInitialAdminState()
return Boolean(state && state.userId === userId && state.mustChangePassword)
}
export async function clearInitialAdminPasswordRequirement(
userId: string,
client: SystemConfigStore = prisma,
options: { throwOnFailure?: boolean } = {}
): Promise<void> {
const state = await getInitialAdminState()
if (!state || state.userId !== userId) {
return
}
try {
await client.systemConfig.delete({
where: { key: INITIAL_ADMIN_STATE_KEY },
})
} catch (error) {
if (options.throwOnFailure) {
throw error
}
console.warn('[SystemInit] 清除初始管理员改密标记失败:', error)
}
}
export async function initializeSystem(): Promise<InitializeSystemResult> {
const initAdminEmail = getInitAdminEmail()
const initAdminUsername = initAdminEmail.split('@')[0] || 'admin'
const result = await prisma.$transaction(async (tx) => {
let adminCreated = false
let adminUpgraded = false
let initialPassword: string | null = null
let passwordSource: PasswordSource | null = null
let admin = await tx.user.findFirst({
where: { isSuperAdmin: true },
})
if (!admin) {
admin = await tx.user.findFirst({
where: {
OR: [
{ email: initAdminEmail },
{ email: 'admin' },
{ username: initAdminUsername },
],
},
})
if (admin) {
if (!admin.isSuperAdmin || admin.role !== 'ADMIN') {
admin = await tx.user.update({
where: { id: admin.id },
data: { isSuperAdmin: true, role: 'ADMIN' },
})
adminUpgraded = true
}
} else {
const resolvedPassword = resolveInitialAdminPassword()
initialPassword = resolvedPassword.password
passwordSource = resolvedPassword.source
const hashedPassword = await bcrypt.hash(resolvedPassword.password, 10)
admin = await tx.user.create({
data: {
email: initAdminEmail,
username: initAdminUsername,
password: hashedPassword,
role: 'ADMIN',
isSuperAdmin: true,
isActive: true,
canDownload: true,
department: '系统管理',
},
})
adminCreated = true
}
}
const ensureDepartment = async () => {
const existing = await tx.department.findFirst({
where: { name: '默认部门' },
})
if (existing) return { entity: existing, created: false }
const created = await tx.department.create({ data: { name: '默认部门' } })
return { entity: created, created: true }
}
const ensureCategory = async () => {
const existing = await tx.category.findFirst({
where: { name: '默认店铺' },
})
if (existing) return { entity: existing, created: false }
const created = await tx.category.create({ data: { name: '默认店铺' } })
return { entity: created, created: true }
}
const ensureScene = async () => {
const existing = await tx.scene.findFirst({
where: { name: '默认场景' },
})
if (existing) return { entity: existing, created: false }
const created = await tx.scene.create({ data: { name: '默认场景' } })
return { entity: created, created: true }
}
const departmentResult = await ensureDepartment()
const categoryResult = await ensureCategory()
const sceneResult = await ensureScene()
return {
admin,
initialPassword,
passwordSource,
createdFlags: {
admin: adminCreated,
adminUpgraded,
department: departmentResult.created,
category: categoryResult.created,
scene: sceneResult.created,
},
defaultDepartment: departmentResult.entity.name,
defaultCategory: categoryResult.entity.name,
defaultScene: sceneResult.entity.name,
}
})
if ((result.createdFlags.admin || result.createdFlags.adminUpgraded) && result.admin) {
await saveInitialAdminState(result.admin.id, result.admin.email)
}
const templatesResult = await ensureBuiltinTemplates()
const alreadyInitialized =
!result.createdFlags.admin &&
!result.createdFlags.adminUpgraded &&
!result.createdFlags.department &&
!result.createdFlags.category &&
!result.createdFlags.scene &&
templatesResult.created.length === 0
return {
initialized: true,
alreadyInitialized,
admin: result.admin
? {
email: result.admin.email,
username: result.admin.username,
created: result.createdFlags.admin,
upgraded: result.createdFlags.adminUpgraded,
mustChangePassword: result.createdFlags.admin || result.createdFlags.adminUpgraded,
initialPassword: result.passwordSource === 'generated' ? result.initialPassword || undefined : undefined,
passwordSource: result.passwordSource || undefined,
}
: null,
defaultDepartment: result.defaultDepartment,
defaultCategory: result.defaultCategory,
defaultScene: result.defaultScene,
templates: {
total: BUILTIN_TEMPLATE_KEYS.length,
created: templatesResult.created.length,
createdTemplates: templatesResult.created,
errors: templatesResult.errors,
},
}
}