From d64581e2d731369a69eb4f002feb8368e4ab02ba Mon Sep 17 00:00:00 2001 From: Kristina M Date: Mon, 27 Apr 2026 13:43:21 +0200 Subject: [PATCH] feat: add audith logs in admin panel and search functionality for incidents --- .../team6backend/admin/AdminController.java | 6 +- .../controller/IncidentController.java | 18 +- .../repository/IncidentRepository.java | 8 + .../incident/service/IncidentService.java | 19 + src/main/resources/static/admin.html | 343 +++++++++++++----- 5 files changed, 297 insertions(+), 97 deletions(-) diff --git a/src/main/java/org/example/team6backend/admin/AdminController.java b/src/main/java/org/example/team6backend/admin/AdminController.java index 0157425..280df9a 100644 --- a/src/main/java/org/example/team6backend/admin/AdminController.java +++ b/src/main/java/org/example/team6backend/admin/AdminController.java @@ -102,7 +102,7 @@ public ResponseEntity updateUserRole(@PathVariable String userId, log.info("PATCH /api/admin/users/{}/role - Admin {} changing role to {}", userId, admin.getGithubLogin(), request.role()); - if (admin.getId().equals(userId)) { // ← FÖRENKLAT kollen + if (admin.getId().equals(userId)) { log.warn("User {} attempted to change their own role", userId); throw new IllegalStateException("You cannot change your own role"); } @@ -156,13 +156,13 @@ public ResponseEntity deleteUser(@PathVariable String userId, log.info("DELETE /api/admin/users/{} - Admin {} attempting to delete user", userId, admin.getGithubLogin()); - if (admin.getId().equals(userId)) { // ← FÖRENKLAT + if (admin.getId().equals(userId)) { log.warn("User {} attempted to delete their own account", userId); throw new IllegalStateException("You cannot delete your own account"); } var userToDelete = userService.getUserById(userId); - userService.deleteUser(userId, admin); // ← SKICKA MED ADMIN + userService.deleteUser(userId, admin); log.info("User {} ({}) deleted by admin {}", userToDelete.getGithubLogin(), userToDelete.getRole(), admin.getGithubLogin()); diff --git a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java index fc92229..ca1e79e 100644 --- a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java +++ b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java @@ -9,6 +9,7 @@ import org.example.team6backend.incident.dto.UpdateIncidentStatusRequest; import org.example.team6backend.incident.entity.Incident; import org.example.team6backend.incident.entity.IncidentCategory; +import org.example.team6backend.incident.entity.IncidentStatus; import org.example.team6backend.incident.service.IncidentService; import org.example.team6backend.notification.service.NotificationService; import org.example.team6backend.security.CustomUserDetails; @@ -112,9 +113,20 @@ public ResponseEntity> getAssignedIncidents( @PreAuthorize("hasRole('ADMIN')") @GetMapping("/all") - public ResponseEntity> getAllIncidents(Pageable pageable) { - log.info("GET /api/incidents/all - Fetching all incidents"); - return ResponseEntity.ok(incidentService.findAll(pageable).map(IncidentResponse::fromEntityBasic)); + public ResponseEntity> getAllIncidents(@RequestParam(required = false) String search, + @RequestParam(required = false) IncidentStatus status, Pageable pageable) { + log.info("GET /api/incidents/all - Fetching all incidents with search={}, status={}", search, status); + + Page incidents; + if (search != null && !search.trim().isEmpty()) { + incidents = incidentService.searchIncidents(search.trim(), pageable); + } else if (status != null) { + incidents = incidentService.findByStatus(status, pageable); + } else { + incidents = incidentService.findAll(pageable); + } + + return ResponseEntity.ok(incidents.map(IncidentResponse::fromEntityBasic)); } @PreAuthorize("hasAnyRole('RESIDENT', 'HANDLER', 'ADMIN')") diff --git a/src/main/java/org/example/team6backend/incident/repository/IncidentRepository.java b/src/main/java/org/example/team6backend/incident/repository/IncidentRepository.java index b1fd485..32e6b0a 100644 --- a/src/main/java/org/example/team6backend/incident/repository/IncidentRepository.java +++ b/src/main/java/org/example/team6backend/incident/repository/IncidentRepository.java @@ -2,6 +2,7 @@ import org.example.team6backend.user.entity.AppUser; import org.example.team6backend.incident.entity.Incident; +import org.example.team6backend.incident.entity.IncidentStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; @@ -22,6 +23,13 @@ public interface IncidentRepository extends JpaRepository { @EntityGraph(attributePaths = {"documents", "createdBy", "assignedTo"}) Page findByAssignedTo(AppUser user, Pageable pageable); + @EntityGraph(attributePaths = {"documents", "createdBy", "assignedTo"}) + Page findByIncidentStatus(IncidentStatus status, Pageable pageable); + @Query("SELECT i FROM Incident i LEFT JOIN FETCH i.documents WHERE i.id = :id") Optional findByIdWithDocuments(@Param("id") Long id); + + @Query("SELECT i FROM Incident i WHERE " + "LOWER(i.subject) LIKE LOWER(CONCAT('%', :search, '%')) OR " + + "LOWER(COALESCE(i.description, '')) LIKE LOWER(CONCAT('%', :search, '%'))") + Page searchIncidents(@Param("search") String search, Pageable pageable); } diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java index 757499f..ead767a 100644 --- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java +++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java @@ -141,6 +141,25 @@ public Page findByAssignedTo(AppUser user, Pageable pageable) { return incidentRepository.findByAssignedTo(user, withDefaultSort(pageable)); } + /** + * Find incidents by status (Admin) + */ + public Page findByStatus(IncidentStatus status, Pageable pageable) { + log.info("Finding incidents by status: {}", status); + return incidentRepository.findByIncidentStatus(status, withDefaultSort(pageable)); + } + + /** + * Search incidents by subject or description (Admin) + */ + public Page searchIncidents(String search, Pageable pageable) { + log.info("Searching incidents with term: {}", search); + if (search == null || search.trim().isEmpty()) { + return incidentRepository.findAll(withDefaultSort(pageable)); + } + return incidentRepository.searchIncidents(search.trim(), withDefaultSort(pageable)); + } + public Incident getById(Long id, AppUser user) { Incident incident = incidentRepository.findByIdWithDocuments(id) .orElseThrow(() -> new ResourceNotFoundException("Not found")); diff --git a/src/main/resources/static/admin.html b/src/main/resources/static/admin.html index 4cc157c..64c4b9c 100644 --- a/src/main/resources/static/admin.html +++ b/src/main/resources/static/admin.html @@ -547,6 +547,14 @@ color: #6b7280; } + .audit-action-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 9999px; + font-size: 0.7rem; + font-weight: 500; + } + @media (max-width: 768px) { .navbar { flex-direction: column; } .filters-bar { flex-direction: column; } @@ -581,7 +589,7 @@

Admin Panel

-

Manage users, roles, and incidents

+

Manage users, roles, incidents and audit logs

@@ -596,6 +604,7 @@

Admin Panel

+
@@ -692,6 +701,41 @@

Admin Panel

+ + - ${incident.incidentCategory} - - ${escapeHtml(incident.createdBy || 'Unknown')} - ${incident.assignedTo ? `✓ ${escapeHtml(incident.assignedTo)}` : 'Unassigned'} - ${formatDate(incident.createdAt)} -
${actionButton}
- + + #${incident.id} +
${escapeHtml(incident.subject)}
${escapeHtml((incident.description || '').substring(0, 50))}${(incident.description || '').length > 50 ? '...' : ''}
+ ${incident.incidentCategory} + + ${escapeHtml(incident.createdBy || 'Unknown')} + ${incident.assignedTo ? `✓ ${escapeHtml(incident.assignedTo)}` : 'Unassigned'} + ${formatDate(incident.createdAt)} +
${actionButton}
+ `; }).join(''); } @@ -1075,6 +1135,42 @@

Assign Incident to Handler

if (e.key === 'Enter') searchIncidents(); }); + async function updateIncidentStatus(incidentId, newStatus) { + try { + const r = await fetch(`/api/incidents/${incidentId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) + }); + if (r.ok) { + showToast('Status updated!', 'success'); + loadIncidents(); + loadStats(); + } else { + const e = await r.text(); + showToast('Failed to update incident status: ' + e, 'error'); + } + } catch(e) { + showToast('Error updating incident status', 'error'); + } + } + + async function unassignIncident(incidentId) { + if (!confirm('Are you sure you want to unassign this incident?')) return; + try { + const r = await fetch(`/api/incidents/${incidentId}/unassign`, { method: 'PATCH' }); + if (r.ok) { + showToast('Incident unassigned successfully!', 'success'); + loadIncidents(); + } else { + const e = await r.text(); + showToast('Failed to unassign incident: ' + e, 'error'); + } + } catch(e) { + showToast('Error unassigning incident', 'error'); + } + } + function openAssignModal(incidentId) { selectedIncidentId = incidentId; if (handlersList.length === 0) { @@ -1112,44 +1208,109 @@

Assign Incident to Handler

} } - async function updateIncidentStatus(incidentId, newStatus) { + function closeAssignModal() { + document.getElementById('assignModal').classList.remove('active'); + selectedIncidentId = null; + } + + async function loadAuditLogs() { + let params = new URLSearchParams(); + params.append('page', currentAuditPage); + params.append('size', auditPageSize); + if (currentAuditSearch) params.append('search', currentAuditSearch); + try { - const r = await fetch(`/api/incidents/${incidentId}/status`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: newStatus }) - }); - if (r.ok) { - showToast('Status updated!', 'success'); - loadIncidents(); - } else { - const e = await r.text(); - showToast('Failed to update incident status: ' + e, 'error'); - } + const r = await fetch('/api/admin/audit?' + params.toString()); + if (!r.ok) throw new Error('Failed to load audit logs'); + const data = await r.json(); + auditTotalPages = data.totalPages; + auditTotalElements = data.totalElements; + renderAuditTable(data.content || []); + renderAuditPagination(); + updateAuditPaginationInfo(); } catch(e) { - showToast('Error updating incident status', 'error'); + console.error(e); + document.getElementById('auditTableBody').innerHTML = 'Error loading audit logs'; } } - async function unassignIncident(incidentId) { - if (!confirm('Are you sure you want to unassign this incident?')) return; - try { - const r = await fetch(`/api/incidents/${incidentId}/unassign`, { method: 'PATCH' }); - if (r.ok) { - showToast('Incident unassigned successfully!', 'success'); - loadIncidents(); - } else { - const e = await r.text(); - showToast('Failed to unassign incident: ' + e, 'error'); - } - } catch(e) { - showToast('Error unassigning incident', 'error'); + function renderAuditTable(logs) { + const tbody = document.getElementById('auditTableBody'); + if (!logs.length) { + tbody.innerHTML = 'No audit logs found'; + return; } + tbody.innerHTML = logs.map(log => ` + + ${formatDate(log.createdAt)} + ${escapeHtml(log.userName || '-')} + ${escapeHtml(log.action)} + ${escapeHtml(log.targetType || '-')} ${log.targetId ? '#' + log.targetId : ''} + ${escapeHtml((log.details || '-').substring(0, 100))}${(log.details || '').length > 100 ? '...' : ''} + + `).join(''); } - function closeAssignModal() { - document.getElementById('assignModal').classList.remove('active'); - selectedIncidentId = null; + function getActionColor(action) { + if (!action) return '#6b7280'; + if (action.includes('CREATE') || action.includes('APPROVE')) return '#10b981'; + if (action.includes('DELETE')) return '#ef4444'; + if (action.includes('UPDATE') || action.includes('EDIT')) return '#3b82f6'; + if (action.includes('ASSIGN')) return '#8b5cf6'; + if (action.includes('CLOSE')) return '#f59e0b'; + if (action.includes('VIEW')) return '#6b7280'; + return '#6b7280'; + } + + function renderAuditPagination() { + const div = document.getElementById('auditPaginationControls'); + if (auditTotalPages <= 1) { + div.innerHTML = ''; + return; + } + let html = ''; + html += ``; + html += ``; + let startPage = Math.max(0, currentAuditPage - 2); + let endPage = Math.min(auditTotalPages - 1, startPage + 4); + for (let i = startPage; i <= endPage; i++) { + html += ``; + } + html += ``; + html += ``; + div.innerHTML = html; + } + + function updateAuditPaginationInfo() { + const start = auditTotalElements === 0 ? 0 : currentAuditPage * auditPageSize + 1; + const end = Math.min((currentAuditPage + 1) * auditPageSize, auditTotalElements); + document.getElementById('auditPaginationInfo').textContent = `Showing ${start}-${end} of ${auditTotalElements} logs`; + } + + function goToAuditPage(page) { + if (page < 0 || page >= auditTotalPages) return; + currentAuditPage = page; + loadAuditLogs(); + } + + function changeAuditPageSize() { + auditPageSize = parseInt(document.getElementById('auditPageSizeSelect').value); + localStorage.setItem('adminAuditPageSize', auditPageSize); + currentAuditPage = 0; + loadAuditLogs(); + } + + function searchAuditLogs() { + currentAuditSearch = document.getElementById('auditSearchInput').value; + currentAuditPage = 0; + loadAuditLogs(); + } + + function clearAuditFilters() { + document.getElementById('auditSearchInput').value = ''; + currentAuditSearch = ''; + currentAuditPage = 0; + loadAuditLogs(); } async function approveUser(userId) {