站长反馈仍然无法编辑其它用户。上一版只把**小程序端**的状态改成小写, 但如果小程序没重新编译(或用的还是旧版本),发上来的仍是 `ACTIVE`, 服务端 `['active','withdrawn','graduate'].includes(status)` 依旧不通过 → 400 「用户状态只能是 active, withdrawn 或 graduate」——编辑任何用户都会失败。 这版把容忍度放到**服务端**,与客户端版本解耦: - 新增 server/utils/user-status.ts: · USER_STATUSES = ['active','withdrawn','graduate'] · normalizeUserStatus() 大小写不敏感('ACTIVE' / ' Active ' → 'active'),认不出返回 null · USER_STATUS_RULE_MESSAGE 统一文案 - [id].put.ts:状态改用归一化值;另修正"自己只能改姓名"里的状态比较也忽略大小写 - [id]/status.put.ts、batch-status.put.ts:同样归一化; 并修掉一个我自己引入的隐患(把归一化结果赋回 const 变量 → TypeError), batch-status 之前被误写成不存在的 statusesRaw,一并改正 效果:无论小程序是否重新编译,管理员都能编辑其它用户(含历史拼音账号只改年级)。
288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
|
||
import { isValidPersonName, isValidUsername, NAME_RULE_MESSAGE, USERNAME_RULE_MESSAGE } from '~~/server/utils/username'
|
||
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'
|
||
import { normalizeUserStatus, USER_STATUS_RULE_MESSAGE } from '~~/server/utils/user-status'
|
||
|
||
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 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)
|
||
|
||
/*
|
||
* 姓名 / 账号名的中文规则:**只校验"这次真的改了"的字段**。
|
||
*
|
||
* 站长反馈「管理员无法编辑其他用户信息了」——根因就在这里:
|
||
* 原来(以及我上一版照搬的写法)是"每次编辑都重新校验姓名和账号名",
|
||
* 于是**历史账号**(早期用拼音/英文建的、或者名字里带「·」「空格」的)
|
||
* 连"改个年级"都会撞「必须纯中文」而被拦死,管理员等于没法维护这些账号。
|
||
* 现在的口径:
|
||
* · 没改动 → 不校验(历史数据**原样保留**,不强迫改名);
|
||
* · 改了 → 必须满足规则(纯中文;admin 账号例外)。
|
||
*/
|
||
const nextUsername = String(username || '').trim()
|
||
const nextName = String(name || '').trim()
|
||
const usernameChanged = nextUsername !== String(targetUser.username || '')
|
||
const nameChanged = nextName !== String(targetUser.name || '')
|
||
|
||
if (usernameChanged && !isValidUsername(nextUsername)) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: USERNAME_RULE_MESSAGE
|
||
})
|
||
}
|
||
if (nameChanged && !isValidPersonName(nextName, nextUsername)) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: NAME_RULE_MESSAGE
|
||
})
|
||
}
|
||
|
||
/*
|
||
* 系统初始超级管理员(ID=1,登录名 admin)的处理 —— 第 90 轮按站长要求放宽。
|
||
*
|
||
* 原来这里是"一律禁止修改"(403 无法修改系统初始超级管理员),
|
||
* 结果是连它的**显示名都改不了**(站长想把界面上的 admin 改成自己的名字)。
|
||
* 现在按"哪些能改、哪些必须锁死"拆开:
|
||
* · 允许:姓名(显示名)、年级、班级、状态;
|
||
* · 锁死:**登录名必须保持 admin** —— 那是 admin/admin123 的应急登录通道,改了就没了;
|
||
* · 锁死:角色不能动(不能把自己降权,否则没人能管站点);
|
||
* · 锁死:密码不在这里改(避免被其它管理员顺手重置)。
|
||
*/
|
||
if (targetUser.id === 1) {
|
||
// 登录名必须还是 admin(usernameChanged 已在上面的中文规则里判过"有没有改")
|
||
if (usernameChanged) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: '初始超级管理员的登录名必须保持 admin(应急登录通道,改了就无法用它登录)'
|
||
})
|
||
}
|
||
if (role && role !== targetUser.role) {
|
||
throw createError({
|
||
statusCode: 403,
|
||
statusMessage: '不能修改初始超级管理员的角色'
|
||
})
|
||
}
|
||
if (password) {
|
||
throw createError({
|
||
statusCode: 403,
|
||
statusMessage: '不能在用户管理中重置初始超级管理员的密码(请用它登录后在「设置」里自行修改)'
|
||
})
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 2. 自己改自己:**只允许改姓名(显示名)**,其余一律禁止。
|
||
*
|
||
* 站长要在「用户管理」里把 admin 的显示名改成自己的名字(登录名仍是 admin),
|
||
* 原来这里是"自己一律不能改",于是他只看到「禁止在用户管理中修改自己的账户」。
|
||
* 放宽到"只有姓名变了、其它字段一个都没动"是安全的:改不了角色/状态/密码/账号名,
|
||
* 也提不了权,只是换个显示名而已(等价于个人资料页改名字)。
|
||
*/
|
||
if (String(userId) === String(user.id)) {
|
||
const gradeChanged = String(grade || '').trim() !== String(targetUser.grade || '').trim()
|
||
const classChanged = String(userClass || '').trim() !== String(targetUser.class || '').trim()
|
||
const statusChanged =
|
||
!!status && String(status).toLowerCase() !== String(targetUser.status || '').toLowerCase()
|
||
const roleChanged = !!role && role !== targetUser.role
|
||
const onlyNameChanged =
|
||
nameChanged && !usernameChanged && !gradeChanged && !classChanged && !statusChanged && !roleChanged && !password
|
||
|
||
if (!onlyNameChanged) {
|
||
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) {
|
||
/*
|
||
* 站长要求用户编辑表单的角色下拉里要有「技术运维」(SUPER_ADMIN)。
|
||
* 原 canChangeRoleOf() 一律禁止提升为 SUPER_ADMIN(网页版也这么设计),
|
||
* 这里对**超级管理员**放开这一条:只有超管能把别人提升为超管,且目标不能是初始账号(ID=1,它本来就是)。
|
||
* 效果与 POST /api/admin/users/{id}/promote-super 一致,两条路都可用。
|
||
*/
|
||
const allowPromoteBySuper =
|
||
user.role === 'SUPER_ADMIN' && role === 'SUPER_ADMIN' && targetUser.id !== 1
|
||
|
||
if (!allowPromoteBySuper && !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} 无法重置目标用户的密码`
|
||
})
|
||
}
|
||
|
||
/*
|
||
* 状态:**大小写不敏感**(客户端历史版本发的是大写的 ACTIVE)。
|
||
* 认不出来才报错 —— 否则管理员会遇到"编辑任何用户都 400"(已踩过)。
|
||
*/
|
||
const nextStatus = status === undefined || status === null ? null : normalizeUserStatus(status)
|
||
if (status !== undefined && status !== null && !nextStatus) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: USER_STATUS_RULE_MESSAGE
|
||
})
|
||
}
|
||
|
||
// 准备更新数据
|
||
const updateData = {
|
||
name,
|
||
username,
|
||
role: validRole,
|
||
grade,
|
||
class: userClass
|
||
}
|
||
|
||
// 如果提供了status,则更新状态相关字段
|
||
if (nextStatus && nextStatus !== existingUser[0].status) {
|
||
updateData.status = nextStatus
|
||
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
|
||
})
|
||
}
|
||
})
|