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:
parent
b5e9cfbcd2
commit
ada95e4e7f
|
|
@ -3,7 +3,7 @@ import { cookies } from 'next/headers'
|
||||||
import { getDb } from '@/lib/db'
|
import { getDb } from '@/lib/db'
|
||||||
import { verifyToken } from '@/lib/auth'
|
import { verifyToken } from '@/lib/auth'
|
||||||
import { checkPermission } from '@/lib/permissions'
|
import { checkPermission } from '@/lib/permissions'
|
||||||
import { writeAuditLog, getClientIP } from '@/lib/audit'
|
import { writeAuditLog, getClientIP, diffObjects } from '@/lib/audit'
|
||||||
|
|
||||||
async function getSession() {
|
async function getSession() {
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
|
|
@ -20,7 +20,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params
|
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 })
|
if (!existing) return NextResponse.json({ error: 'API Key 不存在' }, { status: 404 })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -35,6 +36,20 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||||
is_active !== undefined ? (is_active ? 1 : 0) : 1,
|
is_active !== undefined ? (is_active ? 1 : 0) : 1,
|
||||||
id
|
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 })
|
return NextResponse.json({ success: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : '更新失败'
|
const msg = e instanceof Error ? e.message : '更新失败'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// GET + POST /api/auth/logout — 退出登录(清除 cookie + 302 跳转 /login)
|
// 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'
|
const cookieDomain = process.env.COOKIE_DOMAIN || '.tlyq.ai'
|
||||||
|
|
||||||
|
|
@ -22,4 +23,28 @@ function logoutResponse(): NextResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() { return logoutResponse() }
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getDb } from '@/lib/db'
|
||||||
import { initDatabase } from '@/lib/db-schema'
|
import { initDatabase } from '@/lib/db-schema'
|
||||||
import { getCurrentUser } from '@/lib/auth'
|
import { getCurrentUser } from '@/lib/auth'
|
||||||
import { hasPermission } from '@/lib/permissions'
|
import { hasPermission } from '@/lib/permissions'
|
||||||
|
import { writeAuditLog, getClientIP } from '@/lib/audit'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
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)
|
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 })
|
return NextResponse.json({ success: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : '删除失败'
|
const msg = e instanceof Error ? e.message : '删除失败'
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getDb } from '@/lib/db'
|
||||||
import { initDatabase } from '@/lib/db-schema'
|
import { initDatabase } from '@/lib/db-schema'
|
||||||
import { getCurrentUser } from '@/lib/auth'
|
import { getCurrentUser } from '@/lib/auth'
|
||||||
import { hasPermission } from '@/lib/permissions'
|
import { hasPermission } from '@/lib/permissions'
|
||||||
|
import { writeAuditLog, getClientIP } from '@/lib/audit'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
|
|
@ -43,6 +44,16 @@ export async function POST(request: NextRequest) {
|
||||||
|
|
||||||
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' })
|
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 d = new Date()
|
||||||
const today = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
const today = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||||
const downloadName = `reports_${today}.zip`
|
const downloadName = `reports_${today}.zip`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue