import { Component, OnInit, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AuditLogService } from '../../services/audit.service'; import { AuthService } from '../../services/auth.service'; import { Router } from '@angular/router'; import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types'; @Component({ selector: 'app-admin-audit', standalone: true, imports: [CommonModule, FormsModule], template: `

Audit Logs

@if (!authService.isAuthenticated() || !authService.user()?.isAdmin) {

You must be an admin to view this page.

} @else {
@if (loading()) {
Loading audit logs...
} @else if (logs().length === 0) {
No audit logs found.
} @else {
@for (log of logs(); track log.id) { }
Time User Category Action Target User Details Status
{{ formatDate(log.createdAt) }} @if (log.user) { } @else if (log.userId) { {{ log.userId }} } @else { - } {{ auditService.getCategoryLabel(log.category) }} {{ auditService.getActionLabel(log.action) }} @if (log.targetUser) { } @else if (log.targetUserId) { {{ log.targetUserId }} } @else { - } {{ log.details ?? '-' }} @if (log.details && log.details.length > 50) { {{ expandedRows()[log.id] ? '(click to collapse)' : '(click to expand)' }} } {{ log.success ? '✓' : '✗' }}
} }
`, styles: [` .audit-container { max-width: 1400px; margin: 0 auto; padding: 2rem; } h1 { color: #2d1b4e; margin-bottom: 1.5rem; } .unauthorized { text-align: center; padding: 2rem; background: #fef2f2; border-radius: 8px; color: #991b1b; } .filters { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; align-items: flex-end; background: rgba(255, 255, 255, 0.8); padding: 1rem; border-radius: 8px; border: 1px solid #e5e7eb; } .filter-group { display: flex; flex-direction: column; gap: 0.25rem; } .filter-group label { font-size: 0.85rem; color: #6b7280; } .filter-group select { padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 4px; min-width: 150px; } .refresh-btn { padding: 0.5rem 1rem; background: #8b5cf6; color: white; border: none; border-radius: 4px; cursor: pointer; } .refresh-btn:hover { background: #7c3aed; } .loading, .no-logs { text-align: center; padding: 2rem; color: #6b7280; } .logs-table-container { overflow-x: auto; background: white; border-radius: 8px; border: 1px solid #e5e7eb; } .logs-table { width: 100%; border-collapse: collapse; } .logs-table th, .logs-table td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; } .logs-table th { background: #f9fafb; font-weight: 600; color: #374151; } .logs-table tr:hover { background: #f9fafb; } .logs-table tr.failed { background: #fef2f2; } .logs-table tr.failed:hover { background: #fee2e2; } .time { white-space: nowrap; font-size: 0.85rem; 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; border-radius: 4px; color: white; font-size: 0.75rem; font-weight: 500; } .details { max-width: 400px; cursor: pointer; position: relative; } .details .details-content { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .details.expanded .details-content { white-space: normal; word-break: break-word; } .details .expand-hint { display: block; font-size: 0.7rem; color: #9ca3af; margin-top: 0.25rem; } .details:hover { background: #f3f4f6; } .status-badge { display: inline-block; width: 24px; height: 24px; border-radius: 50%; text-align: center; line-height: 24px; font-weight: bold; } .status-badge.success { background: #dcfce7; color: #16a34a; } .status-badge.failed { background: #fee2e2; color: #dc2626; } .pagination { display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 1rem; padding: 1rem; } .pagination button { padding: 0.5rem 1rem; background: #8b5cf6; color: white; border: none; border-radius: 4px; cursor: pointer; } .pagination button:disabled { background: #d1d5db; cursor: not-allowed; } .pagination button:not(:disabled):hover { background: #7c3aed; } `], }) export class AdminAuditComponent implements OnInit { authService = inject(AuthService); auditService = inject(AuditLogService); private router = inject(Router); logs = signal([]); loading = signal(true); currentPage = signal(1); totalPages = signal(1); expandedRows = signal>({}); selectedCategory = ''; selectedAction = ''; selectedSuccess = ''; ngOnInit() { if (!this.authService.isAuthenticated() || !this.authService.user()?.isAdmin) { this.router.navigate(['/']); return; } this.loadLogs(); } async loadLogs() { this.loading.set(true); try { const filters: Record = { page: this.currentPage(), limit: 50, }; if (this.selectedCategory) { filters['category'] = this.selectedCategory as AuditCategory; } if (this.selectedAction) { filters['action'] = this.selectedAction as AuditAction; } if (this.selectedSuccess !== '') { filters['success'] = this.selectedSuccess === 'true'; } const response = await this.auditService.getLogs(filters); this.logs.set(response.logs); this.totalPages.set(response.totalPages); } catch (error) { console.error('Failed to load audit logs:', error); } finally { this.loading.set(false); } } goToPage(page: number) { this.currentPage.set(page); this.loadLogs(); } formatDate(date: Date | string): string { const d = new Date(date); return d.toLocaleString(); } toggleRowExpand(logId: string) { this.expandedRows.set({ ...this.expandedRows(), [logId]: !this.expandedRows()[logId] }); } }