fix: 补全4处审计日志缺失 + audit_logs.username列迁移

- api-keys/[id] PUT: 添加 diffObjects 变更审计
- reports/[id] DELETE: 添加删除报告快照审计
- auth/logout POST: 从 session cookie 提取 username 记录登出审计
- reports/download POST: 添加 export 审计
- db-schema: 添加 audit_logs.username/api_key_id 列迁移(修复审计日志静默失败)
This commit is contained in:
gitadmin 2026-07-13 16:10:20 +08:00
parent b5e9cfbcd2
commit ada95e4e7f
4 changed files with 66 additions and 4 deletions

View File

@ -3,7 +3,7 @@ import { cookies } from 'next/headers'
import { getDb } from '@/lib/db'
import { verifyToken } from '@/lib/auth'
import { checkPermission } from '@/lib/permissions'
import { writeAuditLog, getClientIP } from '@/lib/audit'
import { writeAuditLog, getClientIP, diffObjects } from '@/lib/audit'
async function getSession() {
const cookieStore = await cookies()
@ -20,7 +20,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
}
const { id } = await params
const existing = getDb().prepare('SELECT id FROM api_keys WHERE id = ?').get(id)
type ApiKeyRow = { id: number; name: string; permissions: string; expires_at: string | null; is_active: number }
const existing = getDb().prepare<[string | number], ApiKeyRow>('SELECT id, name, permissions, expires_at, is_active FROM api_keys WHERE id = ?').get(id)
if (!existing) return NextResponse.json({ error: 'API Key 不存在' }, { status: 404 })
try {
@ -35,6 +36,20 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
is_active !== undefined ? (is_active ? 1 : 0) : 1,
id
)
// 审计日志
const changes = diffObjects(existing as unknown as Record<string, unknown>, body)
if (Object.keys(changes).length > 0) {
writeAuditLog({
userId: session.id,
action: 'update',
entityType: 'api_key',
entityId: Number(id),
details: { changes },
ipAddress: getClientIP(request),
})
}
return NextResponse.json({ success: true })
} catch (e) {
const msg = e instanceof Error ? e.message : '更新失败'

View File

@ -1,5 +1,6 @@
// GET + POST /api/auth/logout — 退出登录(清除 cookie + 302 跳转 /login
import { NextResponse } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { writeAuditLog, getClientIP } from '@/lib/audit'
const cookieDomain = process.env.COOKIE_DOMAIN || '.tlyq.ai'
@ -22,4 +23,28 @@ function logoutResponse(): NextResponse {
}
export async function GET() { return logoutResponse() }
export async function POST() { return logoutResponse() }
export async function POST(request: NextRequest) {
// 从 session cookie 提取用户名用于审计
let username = 'anonymous'
try {
const token =
request.cookies.get('tlyq_session')?.value ||
request.cookies.get('session_issue')?.value
if (token) {
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
username = payload.username || 'unknown'
}
} catch { /* 解析失败,使用默认值 */ }
writeAuditLog({
userId: null,
username,
action: 'logout',
entityType: 'auth',
details: { message: '用户登出' },
ipAddress: getClientIP(request),
})
return logoutResponse()
}

View File

@ -3,6 +3,7 @@ import { getDb } from '@/lib/db'
import { initDatabase } from '@/lib/db-schema'
import { getCurrentUser } from '@/lib/auth'
import { hasPermission } from '@/lib/permissions'
import { writeAuditLog, getClientIP } from '@/lib/audit'
import fs from 'fs'
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
@ -89,6 +90,16 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis
// 删除数据库记录
db.prepare('DELETE FROM reports WHERE id = ?').run(id)
writeAuditLog({
userId: user.id,
apiKeyId: null,
action: 'delete',
entityType: 'report',
entityId: Number(id),
details: { deleted: { id: Number(id), title: report.title, type: report.type, period_start: report.period_start, period_end: report.period_end, file_path: report.file_path } },
ipAddress: getClientIP(_request),
})
return NextResponse.json({ success: true })
} catch (e) {
const msg = e instanceof Error ? e.message : '删除失败'

View File

@ -3,6 +3,7 @@ import { getDb } from '@/lib/db'
import { initDatabase } from '@/lib/db-schema'
import { getCurrentUser } from '@/lib/auth'
import { hasPermission } from '@/lib/permissions'
import { writeAuditLog, getClientIP } from '@/lib/audit'
import JSZip from 'jszip'
import fs from 'fs'
@ -43,6 +44,16 @@ export async function POST(request: NextRequest) {
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' })
writeAuditLog({
userId: user.id,
apiKeyId: null,
action: 'export',
entityType: 'report',
entityId: null,
details: { exported_count: ids.length, ids },
ipAddress: getClientIP(request),
})
const d = new Date()
const today = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
const downloadName = `reports_${today}.zip`