281 lines
7.8 KiB
TypeScript
281 lines
7.8 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 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): Promise<void> {
|
|
const state = await getInitialAdminState()
|
|
if (!state || state.userId !== userId) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await prisma.systemConfig.delete({
|
|
where: { key: INITIAL_ADMIN_STATE_KEY },
|
|
})
|
|
} catch (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.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,
|
|
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,
|
|
},
|
|
}
|
|
}
|