diff --git a/api/src/app/routes/suggestions/index.ts b/api/src/app/routes/suggestions/index.ts index c94631d..9984b58 100644 --- a/api/src/app/routes/suggestions/index.ts +++ b/api/src/app/routes/suggestions/index.ts @@ -83,17 +83,14 @@ export default async function (app: FastifyInstance): Promise { request.body ); - await AuditService.log( - { - action: AuditAction.ENTRY_CREATE, - category: AuditCategory.CONTENT, - resourceType: "Suggestion", - resourceId: suggestion.id, - details: `Created ${suggestion.entityType} suggestion: ${suggestion.title}`, - success: true, - }, - request - ); + await AuditService.logFromRequest(request, { + action: AuditAction.ENTRY_CREATE, + category: AuditCategory.CONTENT, + resourceType: "Suggestion", + resourceId: suggestion.id, + details: `Created ${suggestion.entityType} suggestion: ${suggestion.title}`, + success: true, + }); reply.send(suggestion); } catch (error) { @@ -116,17 +113,14 @@ export default async function (app: FastifyInstance): Promise { try { const suggestion = await SuggestionService.acceptSuggestion(id); - await AuditService.log( - { - action: AuditAction.ENTRY_UPDATE, - category: AuditCategory.ADMIN, - resourceType: "Suggestion", - resourceId: suggestion.id, - details: `Accepted ${suggestion.entityType} suggestion: ${suggestion.title}`, - success: true, - }, - request - ); + await AuditService.logFromRequest(request, { + action: AuditAction.ENTRY_UPDATE, + category: AuditCategory.ADMIN, + resourceType: "Suggestion", + resourceId: suggestion.id, + details: `Accepted ${suggestion.entityType} suggestion: ${suggestion.title}`, + success: true, + }); reply.send(suggestion); } catch (error) { @@ -150,17 +144,14 @@ export default async function (app: FastifyInstance): Promise { try { const suggestion = await SuggestionService.acceptSuggestionWithEdits(id, editedData); - await AuditService.log( - { - action: AuditAction.ENTRY_UPDATE, - category: AuditCategory.ADMIN, - resourceType: "Suggestion", - resourceId: suggestion.id, - details: `Accepted ${suggestion.entityType} suggestion with edits: ${suggestion.title}`, - success: true, - }, - request - ); + await AuditService.logFromRequest(request, { + action: AuditAction.ENTRY_UPDATE, + category: AuditCategory.ADMIN, + resourceType: "Suggestion", + resourceId: suggestion.id, + details: `Accepted ${suggestion.entityType} suggestion with edits: ${suggestion.title}`, + success: true, + }); reply.send(suggestion); } catch (error) { @@ -184,17 +175,14 @@ export default async function (app: FastifyInstance): Promise { try { const suggestion = await SuggestionService.declineSuggestion(id, reason); - await AuditService.log( - { - action: AuditAction.ENTRY_UPDATE, - category: AuditCategory.ADMIN, - resourceType: "Suggestion", - resourceId: suggestion.id, - details: `Declined ${suggestion.entityType} suggestion: ${suggestion.title}${reason ? ` (Reason: ${reason})` : ""}`, - success: true, - }, - request - ); + await AuditService.logFromRequest(request, { + action: AuditAction.ENTRY_UPDATE, + category: AuditCategory.ADMIN, + resourceType: "Suggestion", + resourceId: suggestion.id, + details: `Declined ${suggestion.entityType} suggestion: ${suggestion.title}${reason ? ` (Reason: ${reason})` : ""}`, + success: true, + }); reply.send(suggestion); } catch (error) { diff --git a/api/src/app/services/audit.service.ts b/api/src/app/services/audit.service.ts index aa27859..c45b117 100644 --- a/api/src/app/services/audit.service.ts +++ b/api/src/app/services/audit.service.ts @@ -74,7 +74,7 @@ export const AuditService = { } } - const [logs, total] = await Promise.all([ + const [rawLogs, total] = await Promise.all([ prisma.auditLog.findMany({ where, orderBy: { createdAt: "desc" }, @@ -84,6 +84,45 @@ export const AuditService = { prisma.auditLog.count({ where }), ]); + // Collect all unique user IDs to fetch + const userIds = new Set(); + for (const log of rawLogs) { + if (log.userId) { + userIds.add(log.userId); + } + if (log.targetUserId) { + userIds.add(log.targetUserId); + } + } + + // Fetch all users in one query + const users = userIds.size > 0 + ? await prisma.user.findMany({ + where: { id: { in: Array.from(userIds) } }, + select: { id: true, username: true, avatar: true }, + }) + : []; + + // Create a lookup map + const userMap = new Map(users.map(u => [u.id, { id: u.id, username: u.username, avatar: u.avatar ?? undefined }])); + + // Map logs with user info + const logs = rawLogs.map(log => ({ + id: log.id, + action: log.action, + category: log.category, + userId: log.userId ?? undefined, + user: log.userId ? userMap.get(log.userId) : undefined, + targetUserId: log.targetUserId ?? undefined, + targetUser: log.targetUserId ? userMap.get(log.targetUserId) : undefined, + resourceType: log.resourceType ?? undefined, + resourceId: log.resourceId ?? undefined, + details: log.details ?? undefined, + userAgent: log.userAgent ?? undefined, + success: log.success, + createdAt: log.createdAt, + })); + return { logs, total, diff --git a/apps/frontend/src/app/components/admin/admin-audit.component.ts b/apps/frontend/src/app/components/admin/admin-audit.component.ts index 8a9057d..5d81ca7 100644 --- a/apps/frontend/src/app/components/admin/admin-audit.component.ts +++ b/apps/frontend/src/app/components/admin/admin-audit.component.ts @@ -85,8 +85,10 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types Time + User Category Action + Target User Details Status @@ -95,6 +97,23 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types @for (log of logs(); track log.id) { {{ formatDate(log.createdAt) }} + + @if (log.user) { + + } @else if (log.userId) { + {{ log.userId }} + } @else { + - + } + {{ auditService.getActionLabel(log.action) }} + + @if (log.targetUser) { + + } @else if (log.targetUserId) { + {{ log.targetUserId }} + } @else { + - + } + {{ log.details ?? '-' }} @if (log.details && log.details.length > 50) { @@ -252,6 +288,50 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types color: #6b7280; } + .user-cell { + min-width: 150px; + } + + .user-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .user-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: cover; + } + + .user-details { + display: flex; + flex-direction: column; + } + + .username { + font-weight: 500; + font-size: 0.9rem; + color: #374151; + } + + .user-id { + font-size: 0.7rem; + color: #9ca3af; + font-family: monospace; + } + + .user-id-only { + font-size: 0.75rem; + color: #6b7280; + font-family: monospace; + } + + .no-user { + color: #9ca3af; + } + .category-badge { display: inline-block; padding: 0.25rem 0.5rem; diff --git a/shared-types/src/lib/audit.types.ts b/shared-types/src/lib/audit.types.ts index 2222d7a..e1a2f3a 100644 --- a/shared-types/src/lib/audit.types.ts +++ b/shared-types/src/lib/audit.types.ts @@ -24,12 +24,20 @@ export enum AuditCategory { SECURITY = "SECURITY", } +export interface AuditLogUser { + id: string; + username: string; + avatar?: string; +} + export interface AuditLog { id: string; action: AuditAction; category: AuditCategory; userId?: string; + user?: AuditLogUser; targetUserId?: string; + targetUser?: AuditLogUser; resourceType?: string; resourceId?: string; details?: string;