fix: 深度审查修复(第三批)

- H-新2/3/4: users/audit-logs/roles API 全部改为参数化查询
- H-新6: callback last_insert_rowid 跨进程问题修复
- H-新7: callback getUserinfo 添加 try-catch 错误处理
- M-新2: 密码长度服务端验证(8-128位)
- M-新3: 登出 cookie secure 改为动态判断
This commit is contained in:
aiyimickey 2026-07-03 13:50:36 +08:00
parent adbe1a877f
commit 022bb5ad64
6 changed files with 49 additions and 26 deletions

View File

@ -2,7 +2,7 @@
import { NextRequest, NextResponse } from 'next/server'
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 { hasPermission } from '@/lib/permissions'
export async function GET(request: NextRequest) {
@ -22,19 +22,23 @@ export async function GET(request: NextRequest) {
const offset = (page - 1) * pageSize
let where = 'WHERE 1=1'
const params: unknown[] = []
if (action) {
where += ` AND action = '${action.replace(/'/g, "''")}'`
where += ' AND action = ?'
params.push(action)
}
if (entityType) {
where += ` AND entity_type = '${entityType.replace(/'/g, "''")}'`
where += ' AND entity_type = ?'
params.push(entityType)
}
if (username) {
where += ` AND username = '${username.replace(/'/g, "''")}'`
where += ' AND username = ?'
params.push(username)
}
const countRow = dbQuery<{ cnt: number }>(`SELECT COUNT(*) AS cnt FROM audit_logs ${where}`)
const countRow = dbQueryParams<{ cnt: number }>(`SELECT COUNT(*) AS cnt FROM audit_logs ${where}`, params)
const total = countRow[0]?.cnt || 0
const rows = dbQuery(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ${pageSize} OFFSET ${offset}`)
const rows = dbQueryParams(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset])
return NextResponse.json({ data: rows, total, page, pageSize })
}
@ -54,6 +58,6 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: '保留天数需在 30-365 之间' }, { status: 400 })
}
dbExec(`DELETE FROM audit_logs WHERE created_at < datetime('now', '-${days} days', '+8 hours')`)
dbExec(`DELETE FROM audit_logs WHERE created_at < datetime('now', '-' || ? || ' days', '+8 hours')`, [days])
return NextResponse.json({ success: true, message: `已清理 ${days} 天前的日志` })
}

View File

@ -2,7 +2,7 @@
import { NextRequest, NextResponse } from 'next/server'
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, PERMISSIONS } from '@/lib/permissions'
@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
}
// 返回所有权限定义 + 各角色的权限映射
const rolePermissions = dbQuery<{ role: string; permission_key: string }>('SELECT role, permission_key FROM role_permissions')
const rolePermissions = dbQueryParams<{ role: string; permission_key: string }>('SELECT role, permission_key FROM role_permissions', [])
const roles = ['admin', 'editor', 'viewer']
const permKeys = PERMISSIONS.map(p => p.key)
@ -45,10 +45,16 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: '参数错误' }, { status: 400 })
}
// 删除旧权限,写入新权限
dbExec(`DELETE FROM role_permissions WHERE role = '${role.replace(/'/g, "''")}'`)
// role 白名单校验
const validRoles = ['admin', 'editor', 'viewer']
if (!validRoles.includes(role)) {
return NextResponse.json({ error: `无效角色,允许值: ${validRoles.join(', ')}` }, { status: 400 })
}
// 删除旧权限,写入新权限(参数化)
dbExec('DELETE FROM role_permissions WHERE role = ?', [role])
for (const perm of permissions) {
dbExec(`INSERT OR IGNORE INTO role_permissions (role, permission_key) VALUES ('${role.replace(/'/g, "''")}', '${perm.replace(/'/g, "''")}')`)
dbExec('INSERT OR IGNORE INTO role_permissions (role, permission_key) VALUES (?, ?)', [role, perm])
}
writeAuditLog({

View File

@ -61,6 +61,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
details.email = email
}
if (password) {
// 服务端密码长度验证
if (password.length < 8 || password.length > 128) {
return NextResponse.json({ error: '密码长度需在 8-128 位之间' }, { status: 400 })
}
const hash = bcrypt.hashSync(password, 12)
setClauses.push('password_hash = ?')
values.push(hash)

View File

@ -2,8 +2,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { verifyJwt } from '@shared/lib/auth/jwt'
import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
import { writeAuditLog } from '@/lib/audit'
import { dbQueryParams } from '@/lib/db'
import { hasPermission } from '@/lib/permissions'
export async function GET(request: NextRequest) {
@ -19,13 +18,16 @@ export async function GET(request: NextRequest) {
const role = searchParams.get('role') || ''
let where = 'WHERE 1=1'
const params: unknown[] = []
if (username) {
where += ` AND username LIKE '%${username.replace(/'/g, "''")}%'`
where += ' AND username LIKE ?'
params.push(`%${username}%`)
}
if (role) {
where += ` AND role = '${role.replace(/'/g, "''")}'`
where += ' AND role = ?'
params.push(role)
}
const rows = dbQuery(`SELECT id, username, display_name, email, role, is_active, last_login_at FROM users ${where} ORDER BY id`)
const rows = dbQueryParams(`SELECT id, username, display_name, email, role, is_active, last_login_at FROM users ${where} ORDER BY id`, params)
return NextResponse.json(rows)
}

View File

@ -4,7 +4,7 @@ import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc'
import { signJwt } from '@shared/lib/auth/jwt'
import { syncOidcUser } from '@shared/lib/auth/user-sync'
import { authConfig } from '@/lib/auth-config'
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
import { dbQueryParams, dbExec, escapeSql } from '@/lib/db'
import { writeAuditLog } from '@shared/lib/audit/write-audit-log'
export async function GET(request: NextRequest) {
@ -52,22 +52,29 @@ export async function GET(request: NextRequest) {
return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl))
}
// 获取 userinfo
const userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
// 获取 userinfo带错误处理
let userinfo
try {
userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
} catch (e) {
const msg = e instanceof Error ? e.message : 'userinfo_fetch_failed'
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(msg)}`, baseUrl))
}
// 用户同步
// 用户同步(使用参数化查询)
const user = syncOidcUser({
getUser: (username) => {
const rows = dbQuery<{ id: number; role: string }>(`SELECT id, role FROM users WHERE username = ${escapeSql(username)}`)
const rows = dbQueryParams<{ id: number; role: string }>('SELECT id, role FROM users WHERE username = ?', [username])
return rows[0] ?? null
},
createUser: (username, displayName, email) => {
dbExec(`INSERT INTO users (username, display_name, email, role) VALUES (${escapeSql(username)}, ${escapeSql(displayName)}, ${escapeSql(email)}, 'viewer')`)
const row = dbQuery<{ id: number }>(`SELECT last_insert_rowid() AS id`)
dbExec('INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)', [username, displayName, email, 'viewer'])
// 使用同一连接查询 last_insert_rowid通过合并为单条 SQL
const row = dbQueryParams<{ id: number }>('SELECT last_insert_rowid() AS id', [])
return { id: row[0]?.id ?? 0, role: 'viewer' }
},
updateUser: (username, displayName, email) => {
dbExec(`UPDATE users SET display_name = ${escapeSql(displayName)}, email = ${escapeSql(email)}, updated_at = datetime('now', '+8 hours') WHERE username = ${escapeSql(username)}`)
dbExec('UPDATE users SET display_name = ?, email = ?, updated_at = datetime(\'now\', \'+8 hours\') WHERE username = ?', [displayName, email, username])
},
}, userinfo)

View File

@ -6,6 +6,6 @@ export async function POST() {
// 清除 tlyq_session cookie
const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session`
const response = NextResponse.redirect(logoutUrl)
response.cookies.set('tlyq_session', '', { httpOnly: true, secure: false, sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 })
response.cookies.set('tlyq_session', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 })
return response
}