Files
BYW-VoiceHub/server/api/admin/users/[id].put.ts
T
Henry_Du deb71875c1 fix(users): 自己可改显示名、超管可在编辑里提升超管(修「保存失败/禁止修改自己的账户」)
站长反馈:在 admin 模式下仍无法修改用户信息,弹「禁止在用户管理中修改自己的账户」;
另外角色下拉里没有「技术运维」。

1) 自己改自己:原来一律禁止 → 现在**只允许改姓名(显示名)**,其余(账号名/年级/班级/
   状态/角色/密码)只要动了就仍然拒绝。这样站长能在「用户管理」里把 admin 的显示名
   改成自己的名字(登录名仍是 admin,应急登录不受影响)。
   状态比较忽略大小写:库里是 active,客户端历史版本可能发 ACTIVE,不该因此判定"改了状态"。

2) 超管可在通用编辑接口里把别人提升为 SUPER_ADMIN(站长要求下拉里要有「技术运维」):
   新增 allowPromoteBySuper 例外 —— 仅当登录者是 SUPER_ADMIN、目标角色设为 SUPER_ADMIN、
   且目标不是初始账号(ID=1,本来就是超管)时放行;非超管仍被 canChangeRoleOf 拦住。
   与 POST /api/admin/users/{id}/promote-super 效果一致,两条路都能走。
2026-09-30 22:58:10 +08:00

283 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
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} 无法重置目标用户的密码`
})
}
// 验证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
})
}
})