Files
BYW-VoiceHub/server/api/admin/users/[id].put.ts
T
Henry_Du 792b3d4ef6 feat(users): 初始管理员可改显示名(登录名锁死 admin)+ 新增「提升为超级管理员」
站长诉求:想自定义 admin 账号的资料,但**必须保留 admin/admin123 作为应急登录**;
同时想用自己绑了微信的中文账号做日常管理。

按"哪些能改、哪些必须锁死"拆开处理:

1) server/api/admin/users/[id].put.ts —— 原来对 ID=1 **一律 403**(连显示名都改不了),
   现在:
   · 允许改:姓名(显示名)、年级、班级、状态;
   · 锁死:登录名必须保持 admin(那就是 admin/admin123 的应急通道,改了就登不进去);
   · 锁死:角色不能动(防止把自己降权导致没人能管站点);
   · 锁死:密码不在这里改(避免被其它管理员顺手重置;要改用它登录后去「设置」里改)。

2) 新增 POST /api/admin/users/{id}/promote-super —— 把某个账号提升为超级管理员。
   之所以单开接口而不是放开通用编辑接口的 role:原 `canChangeRoleOf()` 明确禁止提升为
   SUPER_ADMIN,两个前端的角色下拉也故意不给这一项。这里是**显式、可审计**的通道:
   仅 SUPER_ADMIN 可调、不能提升自己、已经是超管的会提示、成功后清该用户认证缓存。

前端(小程序)配套:用户管理操作菜单改成 {label, run} 数组(有权限时才出现"提升为超级管理员",
不再靠写死 tapIndex),点击有二次确认并列出会拿到的权限;编辑初始管理员时登录名输入框 disabled
并显示原因,姓名照常可改。
2026-09-30 21:42:59 +08:00

236 lines
7.8 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: '姓名和用户名为必填项'
})
}
// 账号名必须是纯中文;**admin 账号例外**(以前没有例外,连它自己都编辑不了)
if (!isValidPersonName(name, username) || !isValidUsername(username)) {
throw createError({
statusCode: 400,
statusMessage: NAME_RULE_MESSAGE + ';' + USERNAME_RULE_MESSAGE
})
}
// 检查用户是否存在
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)
/*
* 系统初始超级管理员(ID=1,登录名 admin)的处理 —— 第 90 轮按站长要求放宽。
*
* 原来这里是"一律禁止修改"(403 无法修改系统初始超级管理员),
* 结果是连它的**显示名都改不了**(站长想把界面上的 admin 改成自己的名字)。
* 现在按"哪些能改、哪些必须锁死"拆开:
* · 允许:姓名(显示名)、年级、班级、状态;
* · 锁死:**登录名必须保持 admin** —— 那是 admin/admin123 的应急登录通道,改了就没了;
* · 锁死:角色不能动(不能把自己降权,否则没人能管站点);
* · 锁死:密码不在这里改(避免被其它管理员顺手重置)。
*/
if (targetUser.id === 1) {
const nextUsername = String(username || '').trim()
if (nextUsername && nextUsername !== targetUser.username) {
throw createError({
statusCode: 400,
statusMessage: '初始超级管理员的登录名必须保持 admin(应急登录通道,改了就无法用它登录)'
})
}
if (role && role !== targetUser.role) {
throw createError({
statusCode: 403,
statusMessage: '不能修改初始超级管理员的角色'
})
}
if (password) {
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
})
}
})