- fix(DataAnalysisPanel): 活跃用户卡片固定 400px + overflow-y 独立滚动容器,底部渐变遮罩提示滚动;列表项 tooltip 增加 max-height 与 overflow 截断;新增圆形头像显示与 @error 回退首字母 - feat(admin): 新增 getUserAvatarUrl 统一头像策略链(OAuth identity→Gravatar→DiceBear 7.x initials),stats/realtime、active-users、users/index 三处接口返回 avatarUrl - fix(admin): 建立 SUPER_ADMIN(100)>ADMIN(80)>USER(10) 分级常量与 canChangeRoleOf/canResetPasswordFor 等谓词;reset-password、[id].put、index.post 全面应用级别守卫;前端 UserManager 按钮按权限禁用、availableRoles 按操作者级别过滤 SUPER_ADMIN/ADMIN 等高角色
211 lines
6.3 KiB
TypeScript
211 lines
6.3 KiB
TypeScript
import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
|
|
import { db } from '~/drizzle/db'
|
|
import { users, userStatusLogs } from '~/drizzle/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
import { updateUserPassword } from '~~/server/services/userService'
|
|
import { getRoleLevel, canChangeRoleOf, canResetPasswordFor } from '~~/server/utils/permissions.js'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
// 检查认证和权限
|
|
const user = event.context.user
|
|
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: '没有权限访问'
|
|
})
|
|
}
|
|
|
|
const userId = getRouterParam(event, 'id')
|
|
const body = await readBody(event)
|
|
const { name, username, password, role, grade, class: userClass, status } = body
|
|
|
|
// 验证必填字段
|
|
if (!name || !username) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: '姓名和用户名为必填项'
|
|
})
|
|
}
|
|
|
|
// 验证姓名和用户名只能包含汉字
|
|
const chineseRegex = /^[\u4e00-\u9fa5]+$/
|
|
if (!chineseRegex.test(name) || !chineseRegex.test(username)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: '姓名和用户名只能包含汉字'
|
|
})
|
|
}
|
|
|
|
// 检查用户是否存在
|
|
const existingUser = await db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.id, parseInt(userId)))
|
|
.limit(1)
|
|
|
|
if (existingUser.length === 0) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: '用户不存在'
|
|
})
|
|
}
|
|
|
|
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,
|
|
statusMessage: '无法修改系统初始超级管理员'
|
|
})
|
|
}
|
|
|
|
// 2. 禁止对自身进行任何用户管理操作(包括角色、资料、状态、密码等)
|
|
if (String(userId) === String(user.id)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: '禁止在用户管理中修改自己的账户'
|
|
})
|
|
}
|
|
|
|
// 3. 非超级管理员必须严格高于目标角色级别
|
|
if (user.role !== 'SUPER_ADMIN' && currentLevel <= targetLevel) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: `权限不足:${user.role} 无法操作同级或更高角色的用户`
|
|
})
|
|
}
|
|
|
|
// 检查用户名是否被其他用户使用
|
|
if (username !== targetUser.username) {
|
|
const duplicateUser = await db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.username, username))
|
|
.limit(1)
|
|
|
|
if (duplicateUser.length > 0) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: '用户名已被其他用户使用'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 角色权限控制(统一使用 canChangeRoleOf 语义化检查)
|
|
let validRole = targetUser.role
|
|
if (role && role !== targetUser.role) {
|
|
if (!canChangeRoleOf(user, targetUser, role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
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,
|
|
statusMessage: '用户状态只能是 active, withdrawn 或 graduate'
|
|
})
|
|
}
|
|
|
|
// 准备更新数据
|
|
const updateData = {
|
|
name,
|
|
username,
|
|
role: validRole,
|
|
grade,
|
|
class: userClass
|
|
}
|
|
|
|
// 如果提供了status,则更新状态相关字段
|
|
if (status && status !== existingUser[0].status) {
|
|
updateData.status = status
|
|
updateData.statusChangedAt = new Date()
|
|
updateData.statusChangedBy = user.id
|
|
}
|
|
|
|
// 如果提供了密码,则使用统一服务更新密码
|
|
if (password) {
|
|
await updateUserPassword(parseInt(userId), password, true)
|
|
}
|
|
|
|
// 更新用户其他信息
|
|
const updatedUser = await db
|
|
.update(users)
|
|
.set(updateData)
|
|
.where(eq(users.id, parseInt(userId)))
|
|
.returning({
|
|
id: users.id,
|
|
name: users.name,
|
|
username: users.username,
|
|
role: users.role,
|
|
grade: users.grade,
|
|
class: users.class,
|
|
status: users.status,
|
|
statusChangedAt: users.statusChangedAt,
|
|
statusChangedBy: users.statusChangedBy,
|
|
lastLogin: users.lastLogin,
|
|
lastLoginIp: users.lastLoginIp,
|
|
passwordChangedAt: users.passwordChangedAt,
|
|
createdAt: users.createdAt,
|
|
updatedAt: users.updatedAt
|
|
})
|
|
|
|
// 如果状态发生变更,记录到状态变更日志
|
|
if (status && status !== existingUser[0].status) {
|
|
await db.insert(userStatusLogs).values({
|
|
userId: parseInt(userId),
|
|
oldStatus: existingUser[0].status,
|
|
newStatus: status,
|
|
reason: `管理员${user.name || user.username}修改用户状态`,
|
|
operatorId: user.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],
|
|
message: '用户更新成功'
|
|
}
|
|
} catch (error) {
|
|
console.error('更新用户失败:', error)
|
|
|
|
if (error.statusCode) {
|
|
throw error
|
|
}
|
|
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: '更新用户失败: ' + error.message
|
|
})
|
|
}
|
|
})
|