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 delete: (args: any) => Promise } } 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 { 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 { 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 { 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 { 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, }, } }