94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
// PUT /api/admin/users/[id] — 修改用户(角色、密码等)
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import bcrypt from 'bcryptjs'
|
|
import { verifyJwt } from '@shared/lib/auth/jwt'
|
|
import { authConfig } from '@/lib/auth-config'
|
|
import { dbQueryParams, dbExec } from '@/lib/db'
|
|
import { writeAuditLog } from '@/lib/audit'
|
|
import { hasPermission } from '@/lib/permissions'
|
|
|
|
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
const token = request.cookies.get('tlyq_session')?.value
|
|
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
const payload = verifyJwt(token, authConfig.jwtSecret)
|
|
if (!payload || !hasPermission(String(payload.role || 'viewer'), 'users:manage')) {
|
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
}
|
|
|
|
const { id } = await params
|
|
const userId = Number(id)
|
|
if (!userId || isNaN(userId)) {
|
|
return NextResponse.json({ error: '无效的用户 ID' }, { status: 400 })
|
|
}
|
|
|
|
const body = await request.json()
|
|
const { role, display_name, email, password } = body
|
|
|
|
// 检查用户是否存在(参数化查询)
|
|
const existing = dbQueryParams<{ id: number; username: string }>(
|
|
'SELECT id, username FROM users WHERE id = ?', [userId]
|
|
)
|
|
if (existing.length === 0) {
|
|
return NextResponse.json({ error: '用户不存在' }, { status: 404 })
|
|
}
|
|
|
|
const setClauses: string[] = []
|
|
const values: unknown[] = []
|
|
const details: Record<string, unknown> = {}
|
|
|
|
if (role !== undefined) {
|
|
// 禁止修改系统保留用户的角色
|
|
if (existing[0].username === 'admin' || existing[0].username === 'localadmin') {
|
|
return NextResponse.json({ error: '不能修改系统保留用户的角色' }, { status: 400 })
|
|
}
|
|
// role 白名单校验
|
|
const validRoles = ['admin', 'editor', 'viewer']
|
|
if (!validRoles.includes(role)) {
|
|
return NextResponse.json({ error: `无效角色,允许值: ${validRoles.join(', ')}` }, { status: 400 })
|
|
}
|
|
setClauses.push('role = ?')
|
|
values.push(role)
|
|
details.role = role
|
|
}
|
|
if (display_name !== undefined) {
|
|
setClauses.push('display_name = ?')
|
|
values.push(String(display_name))
|
|
details.display_name = display_name
|
|
}
|
|
if (email !== undefined) {
|
|
setClauses.push('email = ?')
|
|
values.push(String(email) || null)
|
|
details.email = email
|
|
}
|
|
if (password) {
|
|
// 服务端密码长度验证
|
|
if (password.length < 8 || password.length > 128) {
|
|
return NextResponse.json({ error: '密码长度需在 8-128 位之间' }, { status: 400 })
|
|
}
|
|
const hash = await bcrypt.hash(password, 12)
|
|
setClauses.push('password_hash = ?')
|
|
values.push(hash)
|
|
details.password = '***'
|
|
}
|
|
|
|
if (setClauses.length === 0) {
|
|
return NextResponse.json({ error: '无可更新字段' }, { status: 400 })
|
|
}
|
|
setClauses.push("updated_at = datetime('now', '+8 hours')")
|
|
values.push(userId)
|
|
|
|
dbExec(`UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`, values)
|
|
|
|
writeAuditLog({
|
|
userId: Number(payload.sub) || null,
|
|
username: String(payload.username || ''),
|
|
action: 'update_user',
|
|
entityType: 'user',
|
|
entityId: userId,
|
|
details,
|
|
ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1',
|
|
})
|
|
|
|
return NextResponse.json({ success: true })
|
|
}
|