diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 3fa06c3..511ae80 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -48,8 +48,8 @@ export default function UsersPage() { setError('两次输入的密码不一致') return } - if (editForm.password && editForm.password.length < 6) { - setError('密码至少 6 位') + if (editForm.password && editForm.password.length < 8) { + setError('密码至少 8 位') return } diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index 01e3fa5..fc35511 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server' import bcrypt from 'bcryptjs' import { verifyJwt } from '@shared/lib/auth/jwt' import { authConfig } from '@/lib/auth-config' -import { dbQuery, dbExec } from '@/lib/db' +import { dbQueryParams, dbExec } from '@/lib/db' import { writeAuditLog } from '@/lib/audit' import { hasPermission } from '@/lib/permissions' @@ -16,18 +16,24 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ } 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 = dbQuery<{ id: number; username: string }>( - `SELECT id, username FROM users WHERE id = ${Number(id)}` + // 检查用户是否存在(参数化查询) + 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 updates: string[] = [] + const setClauses: string[] = [] + const values: unknown[] = [] const details: Record = {} if (role !== undefined) { @@ -40,36 +46,41 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ if (!validRoles.includes(role)) { return NextResponse.json({ error: `无效角色,允许值: ${validRoles.join(', ')}` }, { status: 400 }) } - updates.push(`role = '${role.replace(/'/g, "''")}'`) + setClauses.push('role = ?') + values.push(role) details.role = role } if (display_name !== undefined) { - updates.push(`display_name = '${String(display_name).replace(/'/g, "''")}'`) + setClauses.push('display_name = ?') + values.push(String(display_name)) details.display_name = display_name } if (email !== undefined) { - updates.push(`email = '${String(email).replace(/'/g, "''")}'`) + setClauses.push('email = ?') + values.push(String(email) || null) details.email = email } if (password) { - const hash = bcrypt.hashSync(password, 10) - updates.push(`password_hash = '${hash.replace(/'/g, "''")}'`) + const hash = bcrypt.hashSync(password, 12) + setClauses.push('password_hash = ?') + values.push(hash) details.password = '***' } - if (updates.length === 0) { + if (setClauses.length === 0) { return NextResponse.json({ error: '无可更新字段' }, { status: 400 }) } - updates.push(`updated_at = datetime('now', '+8 hours')`) + setClauses.push("updated_at = datetime('now', '+8 hours')") + values.push(userId) - dbExec(`UPDATE users SET ${updates.join(', ')} WHERE id = ${Number(id)}`) + 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: Number(id), + entityId: userId, details, ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', }) diff --git a/src/lib/db.ts b/src/lib/db.ts index 5a5a1aa..79c4d67 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -48,10 +48,16 @@ function ensureDb() { } } -// 使用 execFileSync 直接传参,不经过 shell -export function dbExec(sql: string): void { +// 执行 SQL(支持可选参数化) +export function dbExec(sql: string, params?: unknown[]): void { ensureDb() - execFileSync('sqlite3', [DB_PATH, sql], { timeout: 10000 }) + if (params && params.length > 0) { + let i = 0 + const escaped = sql.replace(/\?/g, () => escapeSql(params[i++])) + execFileSync('sqlite3', [DB_PATH, escaped], { timeout: 10000 }) + } else { + execFileSync('sqlite3', [DB_PATH, sql], { timeout: 10000 }) + } } export function dbQuery>(sql: string): T[] {