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:
parent
adbe1a877f
commit
022bb5ad64
|
|
@ -2,7 +2,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||||
import { authConfig } from '@/lib/auth-config'
|
import { authConfig } from '@/lib/auth-config'
|
||||||
import { dbQuery, dbExec } from '@/lib/db'
|
import { dbQueryParams, dbExec } from '@/lib/db'
|
||||||
import { hasPermission } from '@/lib/permissions'
|
import { hasPermission } from '@/lib/permissions'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|
@ -22,19 +22,23 @@ export async function GET(request: NextRequest) {
|
||||||
const offset = (page - 1) * pageSize
|
const offset = (page - 1) * pageSize
|
||||||
|
|
||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
|
const params: unknown[] = []
|
||||||
if (action) {
|
if (action) {
|
||||||
where += ` AND action = '${action.replace(/'/g, "''")}'`
|
where += ' AND action = ?'
|
||||||
|
params.push(action)
|
||||||
}
|
}
|
||||||
if (entityType) {
|
if (entityType) {
|
||||||
where += ` AND entity_type = '${entityType.replace(/'/g, "''")}'`
|
where += ' AND entity_type = ?'
|
||||||
|
params.push(entityType)
|
||||||
}
|
}
|
||||||
if (username) {
|
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 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 })
|
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 })
|
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} 天前的日志` })
|
return NextResponse.json({ success: true, message: `已清理 ${days} 天前的日志` })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||||
import { authConfig } from '@/lib/auth-config'
|
import { authConfig } from '@/lib/auth-config'
|
||||||
import { dbQuery, dbExec } from '@/lib/db'
|
import { dbQueryParams, dbExec } from '@/lib/db'
|
||||||
import { writeAuditLog } from '@/lib/audit'
|
import { writeAuditLog } from '@/lib/audit'
|
||||||
import { hasPermission, PERMISSIONS } from '@/lib/permissions'
|
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 roles = ['admin', 'editor', 'viewer']
|
||||||
const permKeys = PERMISSIONS.map(p => p.key)
|
const permKeys = PERMISSIONS.map(p => p.key)
|
||||||
|
|
||||||
|
|
@ -45,10 +45,16 @@ export async function PUT(request: NextRequest) {
|
||||||
return NextResponse.json({ error: '参数错误' }, { status: 400 })
|
return NextResponse.json({ error: '参数错误' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除旧权限,写入新权限
|
// role 白名单校验
|
||||||
dbExec(`DELETE FROM role_permissions WHERE role = '${role.replace(/'/g, "''")}'`)
|
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) {
|
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({
|
writeAuditLog({
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||||
details.email = email
|
details.email = email
|
||||||
}
|
}
|
||||||
if (password) {
|
if (password) {
|
||||||
|
// 服务端密码长度验证
|
||||||
|
if (password.length < 8 || password.length > 128) {
|
||||||
|
return NextResponse.json({ error: '密码长度需在 8-128 位之间' }, { status: 400 })
|
||||||
|
}
|
||||||
const hash = bcrypt.hashSync(password, 12)
|
const hash = bcrypt.hashSync(password, 12)
|
||||||
setClauses.push('password_hash = ?')
|
setClauses.push('password_hash = ?')
|
||||||
values.push(hash)
|
values.push(hash)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||||
import { authConfig } from '@/lib/auth-config'
|
import { authConfig } from '@/lib/auth-config'
|
||||||
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
|
import { dbQueryParams } from '@/lib/db'
|
||||||
import { writeAuditLog } from '@/lib/audit'
|
|
||||||
import { hasPermission } from '@/lib/permissions'
|
import { hasPermission } from '@/lib/permissions'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|
@ -19,13 +18,16 @@ export async function GET(request: NextRequest) {
|
||||||
const role = searchParams.get('role') || ''
|
const role = searchParams.get('role') || ''
|
||||||
|
|
||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
|
const params: unknown[] = []
|
||||||
if (username) {
|
if (username) {
|
||||||
where += ` AND username LIKE '%${username.replace(/'/g, "''")}%'`
|
where += ' AND username LIKE ?'
|
||||||
|
params.push(`%${username}%`)
|
||||||
}
|
}
|
||||||
if (role) {
|
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)
|
return NextResponse.json(rows)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc'
|
||||||
import { signJwt } from '@shared/lib/auth/jwt'
|
import { signJwt } from '@shared/lib/auth/jwt'
|
||||||
import { syncOidcUser } from '@shared/lib/auth/user-sync'
|
import { syncOidcUser } from '@shared/lib/auth/user-sync'
|
||||||
import { authConfig } from '@/lib/auth-config'
|
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'
|
import { writeAuditLog } from '@shared/lib/audit/write-audit-log'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
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))
|
return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 userinfo
|
// 获取 userinfo(带错误处理)
|
||||||
const userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
|
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({
|
const user = syncOidcUser({
|
||||||
getUser: (username) => {
|
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
|
return rows[0] ?? null
|
||||||
},
|
},
|
||||||
createUser: (username, displayName, email) => {
|
createUser: (username, displayName, email) => {
|
||||||
dbExec(`INSERT INTO users (username, display_name, email, role) VALUES (${escapeSql(username)}, ${escapeSql(displayName)}, ${escapeSql(email)}, 'viewer')`)
|
dbExec('INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)', [username, displayName, email, 'viewer'])
|
||||||
const row = dbQuery<{ id: number }>(`SELECT last_insert_rowid() AS id`)
|
// 使用同一连接查询 last_insert_rowid(通过合并为单条 SQL)
|
||||||
|
const row = dbQueryParams<{ id: number }>('SELECT last_insert_rowid() AS id', [])
|
||||||
return { id: row[0]?.id ?? 0, role: 'viewer' }
|
return { id: row[0]?.id ?? 0, role: 'viewer' }
|
||||||
},
|
},
|
||||||
updateUser: (username, displayName, email) => {
|
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)
|
}, userinfo)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ export async function POST() {
|
||||||
// 清除 tlyq_session cookie
|
// 清除 tlyq_session cookie
|
||||||
const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session`
|
const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session`
|
||||||
const response = NextResponse.redirect(logoutUrl)
|
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
|
return response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue