forked from BRETT/BRETT
New audit_log table records actor, action, target and a JSON snapshot of the details, so entries stay readable after the target is deleted; the actor is set to NULL when the user is removed. logAudit() is called from upload delete (single and bulk), comment delete, tag removal, role create/delete, role permission changes, role assign/revoke and MOTD updates. Logging failures only warn and never fail the action itself. The admin page gets an audit card behind the new audit.view permission with action filter, click-to-filter by user, readable summaries per action and server-side pagination. Action catalog lives in shared/utils/audit.ts, labels under admin.auditActions (de/en). Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import { and, count, desc, eq, type SQL } from 'drizzle-orm'
|
|
import { AUDIT_ACTIONS } from '../../../shared/utils/audit'
|
|
import { auditLog, users } from '../../db/schema'
|
|
|
|
const PER_PAGE = 25
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
await requirePermission(event, 'audit.view')
|
|
const q = getQuery(event)
|
|
const page = Math.max(1, Math.floor(Number(q.page)) || 1)
|
|
const action = String(q.action ?? '')
|
|
const actorId = Number(q.user)
|
|
|
|
const conditions: SQL[] = []
|
|
if ((AUDIT_ACTIONS as readonly string[]).includes(action)) conditions.push(eq(auditLog.action, action))
|
|
if (Number.isInteger(actorId) && actorId > 0) conditions.push(eq(auditLog.userId, actorId))
|
|
const where = conditions.length ? and(...conditions) : undefined
|
|
|
|
const db = useDb()
|
|
const [totalRow, rows] = await Promise.all([
|
|
db.select({ count: count() }).from(auditLog).where(where),
|
|
db
|
|
.select({
|
|
id: auditLog.id,
|
|
action: auditLog.action,
|
|
targetType: auditLog.targetType,
|
|
targetId: auditLog.targetId,
|
|
details: auditLog.details,
|
|
createdAt: auditLog.createdAt,
|
|
userId: auditLog.userId,
|
|
userName: users.name,
|
|
userColor: users.color,
|
|
userAvatar: users.avatar,
|
|
})
|
|
.from(auditLog)
|
|
.leftJoin(users, eq(auditLog.userId, users.id))
|
|
.where(where)
|
|
.orderBy(desc(auditLog.id))
|
|
.limit(PER_PAGE)
|
|
.offset((page - 1) * PER_PAGE),
|
|
])
|
|
|
|
const total = totalRow[0]?.count ?? 0
|
|
return {
|
|
items: rows.map(({ userAvatar, ...r }) => ({ ...r, userAvatarUrl: avatarUrl(userAvatar) })),
|
|
total,
|
|
perPage: PER_PAGE,
|
|
pageCount: Math.max(1, Math.ceil(total / PER_PAGE)),
|
|
}
|
|
})
|