feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -3,27 +3,7 @@ import { db } from '~/drizzle/db'
|
||||
import { users, userStatusLogs } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { updateUserPassword } from '~~/server/services/userService'
|
||||
import { createSystemNotification } from '~~/server/services/notificationService'
|
||||
import {
|
||||
PASSWORD_AUDIT_ACTIONS,
|
||||
getPasswordAuditContext
|
||||
} from '~~/server/services/passwordSecurityService'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import { getAdminPasswordViolation } from '~~/server/utils/admin-password-policy'
|
||||
|
||||
const normalizeRequiredText = (value: unknown) => String(value || '').trim()
|
||||
const normalizeOptionalText = (value: unknown) => {
|
||||
const normalized = String(value || '').trim()
|
||||
return normalized || null
|
||||
}
|
||||
|
||||
const roleNames: Record<string, string> = {
|
||||
USER: '用户',
|
||||
SONG_ADMIN: '歌曲管理员',
|
||||
ADMIN: '管理员',
|
||||
SUPER_ADMIN: '超级管理员'
|
||||
}
|
||||
import { getRoleLevel, canChangeRoleOf, canResetPasswordFor } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -32,175 +12,145 @@ export default defineEventHandler(async (event) => {
|
||||
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '没有权限访问'
|
||||
statusMessage: '没有权限访问'
|
||||
})
|
||||
}
|
||||
|
||||
const userId = getRouterParam(event, 'id')
|
||||
const userIdNum = Number.parseInt(String(userId), 10)
|
||||
if (!Number.isInteger(userIdNum) || userIdNum <= 0) {
|
||||
const body = await readBody(event)
|
||||
const { name, username, password, role, grade, class: userClass, status } = body
|
||||
|
||||
// 验证必填字段
|
||||
if (!name || !username) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '无效的用户ID'
|
||||
statusMessage: '姓名和用户名为必填项'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { name, username, password, role, grade, class: userClass, status } = body || {}
|
||||
const normalizedName = normalizeRequiredText(name)
|
||||
const normalizedUsername = normalizeRequiredText(username)
|
||||
|
||||
// 验证必填字段
|
||||
if (!normalizedName || !normalizedUsername) {
|
||||
// 验证姓名和用户名只能包含汉字
|
||||
const chineseRegex = /^[\u4e00-\u9fa5]+$/
|
||||
if (!chineseRegex.test(name) || !chineseRegex.test(username)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '姓名和用户名为必填项'
|
||||
statusMessage: '姓名和用户名只能包含汉字'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await db.select().from(users).where(eq(users.id, userIdNum)).limit(1)
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, parseInt(userId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: '用户不存在'
|
||||
statusMessage: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
const targetUser = existingUser[0]!
|
||||
const targetUser = existingUser[0]
|
||||
const currentLevel = getRoleLevel(user.role)
|
||||
const targetLevel = getRoleLevel(targetUser.role)
|
||||
|
||||
// 1. 禁止修改系统初始超级管理员 (ID: 1)
|
||||
if (targetUser.id === 1) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '无法修改系统初始超级管理员'
|
||||
statusMessage: '无法修改系统初始超级管理员'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 禁止对自身进行任何用户管理操作(包括角色、资料、状态、密码等)
|
||||
// 使用 String 转换确保 ID 比较的准确性
|
||||
if (String(userId) === String(user.id)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '禁止在用户管理中修改自己的账户'
|
||||
statusMessage: '禁止在用户管理中修改自己的账户'
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 越级修改保护
|
||||
// 如果目标用户是 SUPER_ADMIN,操作者必须是 SUPER_ADMIN
|
||||
if (targetUser.role === 'SUPER_ADMIN' && user.role !== 'SUPER_ADMIN') {
|
||||
// 3. 非超级管理员必须严格高于目标角色级别
|
||||
if (user.role !== 'SUPER_ADMIN' && currentLevel <= targetLevel) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '权限不足:普通管理员无法修改超级管理员信息'
|
||||
statusMessage: `权限不足:${user.role} 无法操作同级或更高角色的用户`
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户名是否被其他用户使用
|
||||
if (normalizedUsername !== targetUser.username) {
|
||||
if (username !== targetUser.username) {
|
||||
const duplicateUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, normalizedUsername))
|
||||
.where(eq(users.username, username))
|
||||
.limit(1)
|
||||
|
||||
if (duplicateUser.length > 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '用户名已被其他用户使用'
|
||||
statusMessage: '用户名已被其他用户使用'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 角色权限控制
|
||||
// 角色权限控制(统一使用 canChangeRoleOf 语义化检查)
|
||||
let validRole = targetUser.role
|
||||
if (role) {
|
||||
if (!['USER', 'ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'].includes(role)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '无效的用户角色'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (role && role !== targetUser.role) {
|
||||
// 超级管理员可以设置任何角色
|
||||
if (user.role === 'SUPER_ADMIN') {
|
||||
validRole = role
|
||||
}
|
||||
// 管理员只能设置管理员以下的角色(USER, SONG_ADMIN)
|
||||
else if (user.role === 'ADMIN') {
|
||||
if (['USER', 'SONG_ADMIN'].includes(role)) {
|
||||
validRole = role
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '管理员只能设置用户和歌曲管理员角色'
|
||||
})
|
||||
}
|
||||
}
|
||||
// 其他角色不能设置角色
|
||||
else {
|
||||
if (!canChangeRoleOf(user, targetUser, role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '没有权限设置用户角色'
|
||||
statusMessage: user.role === 'SUPER_ADMIN'
|
||||
? '超级管理员不能将目标用户提升为 SUPER_ADMIN'
|
||||
: `权限不足:${user.role} 无法将用户角色更改为 ${role}`
|
||||
})
|
||||
}
|
||||
validRole = role
|
||||
}
|
||||
|
||||
// 如果请求中包含密码更新,校验密码重置权限
|
||||
if (password && !canResetPasswordFor(user, targetUser)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: `权限不足:${user.role} 无法重置目标用户的密码`
|
||||
})
|
||||
}
|
||||
|
||||
// 验证status字段的有效性
|
||||
if (status && !['active', 'withdrawn', 'graduate'].includes(status)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '用户状态只能是 active, withdrawn 或 graduate'
|
||||
statusMessage: '用户状态只能是 active, withdrawn 或 graduate'
|
||||
})
|
||||
}
|
||||
|
||||
// 待审核用户不得通过编辑接口直接置为 active(须走注册审核流程)
|
||||
if (targetUser.status === 'pending' && status === 'active') {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.USER_NOT_PENDING, '待审核用户需通过注册审核流程处理')
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
const updateData = {
|
||||
name: normalizedName,
|
||||
username: normalizedUsername,
|
||||
name,
|
||||
username,
|
||||
role: validRole,
|
||||
grade: normalizeOptionalText(grade),
|
||||
class: normalizeOptionalText(userClass),
|
||||
...(status && status !== targetUser.status
|
||||
? {
|
||||
status,
|
||||
statusChangedAt: new Date(),
|
||||
statusChangedBy: user.id
|
||||
}
|
||||
: {})
|
||||
grade,
|
||||
class: userClass
|
||||
}
|
||||
|
||||
// 如果提供了status,则更新状态相关字段
|
||||
if (status && status !== existingUser[0].status) {
|
||||
updateData.status = status
|
||||
updateData.statusChangedAt = new Date()
|
||||
updateData.statusChangedBy = user.id
|
||||
}
|
||||
|
||||
// 如果提供了密码,则使用统一服务更新密码
|
||||
if (password) {
|
||||
const trimmedPassword = String(password).trim()
|
||||
// 管理员重置为临时密码且 forceReset 强制用户登录后修改,仅做基础校验不要求完整复杂度
|
||||
const violation = getAdminPasswordViolation(trimmedPassword)
|
||||
if (violation) {
|
||||
throw createApiError(400, violation.code, violation.message)
|
||||
}
|
||||
|
||||
await updateUserPassword(targetUser.id, trimmedPassword, {
|
||||
forceReset: true,
|
||||
auditContext: {
|
||||
action: PASSWORD_AUDIT_ACTIONS.RESET_PASSWORD,
|
||||
actorId: user.id,
|
||||
...getPasswordAuditContext(event)
|
||||
}
|
||||
})
|
||||
await updateUserPassword(parseInt(userId), password, true)
|
||||
}
|
||||
|
||||
// 更新用户其他信息
|
||||
const updatedUser = await db
|
||||
.update(users)
|
||||
.set(updateData)
|
||||
.where(eq(users.id, userIdNum))
|
||||
.where(eq(users.id, parseInt(userId)))
|
||||
.returning({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
@@ -219,35 +169,33 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
|
||||
// 如果状态发生变更,记录到状态变更日志
|
||||
if (status && status !== targetUser.status) {
|
||||
if (status && status !== existingUser[0].status) {
|
||||
await db.insert(userStatusLogs).values({
|
||||
userId: targetUser.id,
|
||||
oldStatus: targetUser.status,
|
||||
userId: parseInt(userId),
|
||||
oldStatus: existingUser[0].status,
|
||||
newStatus: status,
|
||||
reason: `管理员${user.name || user.username}修改用户状态`,
|
||||
operatorId: user.id
|
||||
})
|
||||
}
|
||||
|
||||
if (validRole !== targetUser.role) {
|
||||
const oldRoleName = roleNames[targetUser.role] || targetUser.role
|
||||
const newRoleName = roleNames[validRole] || validRole
|
||||
const notification = await createSystemNotification(
|
||||
targetUser.id,
|
||||
'权限变更通知',
|
||||
`您的账户权限已由系统更新:${oldRoleName} → ${newRoleName}`
|
||||
)
|
||||
if (!notification) {
|
||||
console.warn(`未向用户 ${targetUser.id} 发送权限变更通知`)
|
||||
}
|
||||
// 清除相关缓存
|
||||
try {
|
||||
const { cache } = await import('~~/server/utils/cache-helpers')
|
||||
await cache.deletePattern('songs:*')
|
||||
// 清除该用户的认证缓存(角色或权限可能已变更)
|
||||
await cache.delete(`auth:user:${parseInt(userId)}`)
|
||||
console.log('[Cache] 歌曲和用户认证缓存已清除(用户更新)')
|
||||
} catch (cacheError) {
|
||||
console.warn('[Cache] 清除缓存失败:', cacheError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: updatedUser[0]!,
|
||||
user: updatedUser[0],
|
||||
message: '用户更新成功'
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
@@ -256,7 +204,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '更新用户失败: ' + error.message
|
||||
statusMessage: '更新用户失败: ' + error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user