站长要求:账号名除了 admin 其它都必须是中文名。 原来「只能包含汉字」的校验散在 5 个写入路径上,而且**没有 admin 例外** —— 导致连 admin 账号自己都编辑不了(改任何字段都撞「姓名和用户名只能包含汉字」)。 - 新增 server/utils/username.ts:规则一处定义 · isValidUsername:纯中文,或恰好是 admin · isValidPersonName:姓名纯中文,但 admin 账号的姓名不做限制 · 统一提示文案(接口报错与客户端提示共用同一句) - 5 个写入路径全部改用它,并删掉各自的 chineseRegex: admin/users 新建、admin/users 编辑、admin/users 批量导入、auth/register、user/profile - 注册时明确拒绝 admin(系统保留账号) 已验证:两端规则对 9 个用例结论完全一致;5 处均无旧正则; admin 可编辑,拼音名(zhangsan / zhang.san / 张三1 / 带空格)一律被拦。
126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
import bcrypt from 'bcryptjs'
|
||
import { isValidPersonName, isValidUsername, NAME_RULE_MESSAGE, USERNAME_RULE_MESSAGE } from '~~/server/utils/username'
|
||
import { db } from '~/drizzle/db'
|
||
import { users } from '~/drizzle/schema'
|
||
import { eq } from 'drizzle-orm'
|
||
import { getRoleLevel, isRoleHigher, canManageUserRole } from '~~/server/utils/permissions'
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
// 检查认证和权限
|
||
const user = event.context.user
|
||
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||
throw createError({
|
||
statusCode: 403,
|
||
message: '没有权限访问'
|
||
})
|
||
}
|
||
|
||
const body = await readBody(event)
|
||
|
||
// 验证必填字段
|
||
if (!body.name || !body.username || !body.password) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '姓名、账号名和密码不能为空'
|
||
})
|
||
}
|
||
|
||
// 账号名必须是纯中文(唯一例外是管理员账号 admin —— 规则见 utils/username.ts)
|
||
if (!isValidPersonName(body.name, body.username) || !isValidUsername(body.username)) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: NAME_RULE_MESSAGE + ';' + USERNAME_RULE_MESSAGE
|
||
})
|
||
}
|
||
|
||
try {
|
||
// 检查用户名是否已存在
|
||
const existingUserResult = await db
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.username, body.username))
|
||
.limit(1)
|
||
const existingUser = existingUserResult[0]
|
||
|
||
if (existingUser) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '账号名已存在'
|
||
})
|
||
}
|
||
|
||
// 加密密码
|
||
const hashedPassword = await bcrypt.hash(body.password, 10)
|
||
|
||
// 角色权限控制(统一使用 canManageUserRole 数值化级别检查)
|
||
let validRole = 'USER'
|
||
const requestedRole = body.role
|
||
if (requestedRole) {
|
||
if (!canManageUserRole(user, requestedRole)) {
|
||
const currentLevel = getRoleLevel(user.role)
|
||
const targetLevel = getRoleLevel(requestedRole)
|
||
throw createError({
|
||
statusCode: 403,
|
||
message: isRoleHigher(requestedRole, user.role) || currentLevel <= targetLevel
|
||
? `权限不足:${user.role} 无法创建级别高于或等于自身的角色(${requestedRole})`
|
||
: `没有权限创建 ${requestedRole} 角色的用户`
|
||
})
|
||
}
|
||
validRole = requestedRole
|
||
}
|
||
|
||
// 状态验证
|
||
let validStatus = 'active'
|
||
if (body.status && ['active', 'withdrawn', 'graduate'].includes(body.status)) {
|
||
validStatus = body.status
|
||
}
|
||
|
||
// 创建用户
|
||
const newUserResult = await db
|
||
.insert(users)
|
||
.values({
|
||
name: body.name,
|
||
username: body.username,
|
||
password: hashedPassword,
|
||
role: validRole,
|
||
status: validStatus as 'active' | 'withdrawn' | 'graduate',
|
||
grade: body.grade,
|
||
class: body.class
|
||
})
|
||
.returning({
|
||
id: users.id,
|
||
name: users.name,
|
||
username: users.username,
|
||
role: users.role,
|
||
status: users.status,
|
||
grade: users.grade,
|
||
class: users.class,
|
||
createdAt: users.createdAt,
|
||
updatedAt: users.updatedAt
|
||
})
|
||
const newUser = newUserResult[0]
|
||
|
||
// 清除相关缓存
|
||
try {
|
||
const { cache } = await import('~~/server/utils/cache-helpers')
|
||
await cache.deletePattern('songs:*')
|
||
await cache.deletePattern('stats:*')
|
||
console.log('[Cache] 歌曲和统计缓存已清除(用户创建)')
|
||
} catch (cacheError) {
|
||
console.warn('[Cache] 清除缓存失败:', cacheError)
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
user: newUser,
|
||
message: '用户创建成功'
|
||
}
|
||
} catch (error) {
|
||
console.error('创建用户失败:', error)
|
||
throw createError({
|
||
statusCode: 500,
|
||
message: '创建用户失败'
|
||
})
|
||
}
|
||
})
|