Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 37 additions & 26 deletions src/main/java/org/example/team6backend/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import org.springframework.security.access.AccessDeniedException;

@RestController
@RequestMapping("/api/admin")
Expand All @@ -32,6 +34,17 @@ public class AdminController {
private final UserService userService;
private final UserMapper userMapper;

private AppUser getCurrentUser(CustomUserDetails currentUser) {
if (currentUser != null && currentUser.getUser() != null) {
return currentUser.getUser();
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
return customUserDetails.getUser();
}
throw new AccessDeniedException("NNot authenticated!");
}
Comment thread
SandraNelj marked this conversation as resolved.

@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(@RequestParam(required = false) String email,
@RequestParam(required = false) String name, @RequestParam(required = false) UserRole role,
Expand Down Expand Up @@ -68,9 +81,13 @@ public ResponseEntity<UserResponse> getUser(@PathVariable String userId) {
}

@PostMapping("/users/{userId}/approve")
public ResponseEntity<UserResponse> approveUser(@PathVariable String userId) {
public ResponseEntity<UserResponse> approveUser(@PathVariable String userId,
@AuthenticationPrincipal CustomUserDetails currentUser) {
AppUser admin = getCurrentUser(currentUser);

log.info("POST /api/admin/users/{}/approve - Approving pending user", userId);
AppUser approvedUser = userService.approvePendingUser(userId);

AppUser approvedUser = userService.approvePendingUser(userId, admin);
log.info("User {} approved successfully. New role: {}", approvedUser.getGithubLogin(), approvedUser.getRole());

return ResponseEntity.ok(userMapper.toResponse(approvedUser));
Expand All @@ -80,14 +97,12 @@ public ResponseEntity<UserResponse> approveUser(@PathVariable String userId) {
public ResponseEntity<UserResponse> updateUserRole(@PathVariable String userId,
@Valid @RequestBody UpdateUserRoleRequest request, @AuthenticationPrincipal CustomUserDetails currentUser) {

String currentUserLogin = currentUser != null && currentUser.getUser() != null
? currentUser.getUser().getGithubLogin()
: "unknown";
AppUser admin = getCurrentUser(currentUser);

log.info("PATCH /api/admin/users/{}/role - Admin {} changing role to {}", userId, currentUserLogin,
log.info("PATCH /api/admin/users/{}/role - Admin {} changing role to {}", userId, admin.getGithubLogin(),
request.role());

if (currentUser != null && currentUser.getUser() != null && currentUser.getUser().getId().equals(userId)) {
if (admin.getId().equals(userId)) { // ← FÖRENKLAT kollen
log.warn("User {} attempted to change their own role", userId);
throw new IllegalStateException("You cannot change your own role");
}
Expand All @@ -97,15 +112,15 @@ public ResponseEntity<UserResponse> updateUserRole(@PathVariable String userId,
if (targetUser.getRole() == UserRole.ADMIN) {
long adminCount = userService.getAllUsers().stream().filter(u -> u.getRole() == UserRole.ADMIN).count();
if (adminCount <= 1) {
log.warn("Attempt to remove last admin user {} by {}", userId, currentUserLogin);
log.warn("Attempt to remove last admin user {} by {}", userId, admin.getGithubLogin());
throw new IllegalStateException("Cannot remove the last admin user");
}
}
}

AppUser updatedUser = userService.updateUserRole(userId, request.role());
AppUser updatedUser = userService.updateUserRole(userId, request.role(), admin); // ← SKICKA MED ADMIN
log.info("User {} role changed to {} by admin {}", updatedUser.getGithubLogin(), request.role(),
currentUserLogin);
admin.getGithubLogin());

return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}
Expand All @@ -115,22 +130,20 @@ public ResponseEntity<UserResponse> updateUserStatus(@PathVariable String userId
@Valid @RequestBody UpdateUserStatusRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser) {

String currentUserLogin = currentUser != null && currentUser.getUser() != null
? currentUser.getUser().getGithubLogin()
: "unknown";
AppUser admin = getCurrentUser(currentUser);

log.info("PATCH /api/admin/users/{}/status - Admin {} setting active={}", userId, currentUserLogin,
log.info("PATCH /api/admin/users/{}/status - Admin {} setting active={}", userId, admin.getGithubLogin(),
request.active());

if (currentUser != null && currentUser.getUser() != null && currentUser.getUser().getId().equals(userId)
&& !request.active()) {
if (admin.getId().equals(userId) && !request.active()) {
log.warn("User {} attempted to deactivate their own account", userId);
throw new IllegalStateException("You cannot deactivate your own account");
}

AppUser updatedUser = userService.updateUserActiveStatus(userId, request.active());
AppUser updatedUser = userService.updateUserActiveStatus(userId, request.active(), admin);

log.info("User {} active status changed to {} by admin {}", updatedUser.getGithubLogin(),
updatedUser.isActive(), currentUserLogin);
updatedUser.isActive(), admin.getGithubLogin());

return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}
Expand All @@ -139,21 +152,19 @@ public ResponseEntity<UserResponse> updateUserStatus(@PathVariable String userId
public ResponseEntity<Void> deleteUser(@PathVariable String userId,
@AuthenticationPrincipal CustomUserDetails currentUser) {

String currentUserLogin = currentUser != null && currentUser.getUser() != null
? currentUser.getUser().getGithubLogin()
: "unknown";
AppUser admin = getCurrentUser(currentUser);

log.info("DELETE /api/admin/users/{} - Admin {} attempting to delete user", userId, currentUserLogin);
log.info("DELETE /api/admin/users/{} - Admin {} attempting to delete user", userId, admin.getGithubLogin());

if (currentUser != null && currentUser.getUser() != null && currentUser.getUser().getId().equals(userId)) {
if (admin.getId().equals(userId)) { // ← FÖRENKLAT
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);
userService.deleteUser(userId, admin); // ← SKICKA MED ADMIN
log.info("User {} ({}) deleted by admin {}", userToDelete.getGithubLogin(), userToDelete.getRole(),
currentUserLogin);
admin.getGithubLogin());

return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.example.team6backend.auditlog.controller;

import lombok.RequiredArgsConstructor;
import org.example.team6backend.auditlog.entity.AuditLog;
import org.example.team6backend.auditlog.repository.AuditLogRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/admin/audit")
@PreAuthorize("hasRole('ADMIN')")
@RequiredArgsConstructor
public class AuditLogController {

private final AuditLogRepository auditLogRepository;

@GetMapping
public ResponseEntity<Page<AuditLog>> getLogs(@RequestParam(required = false) String search,
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<AuditLog> logs;
if (search != null && !search.isEmpty()) {
logs = auditLogRepository.searchLogs(search, pageable);
} else {
logs = auditLogRepository.findAll(pageable);
}
return ResponseEntity.ok(logs);
}
Comment thread
SandraNelj marked this conversation as resolved.

@GetMapping("/user/{userId}")
public ResponseEntity<Page<AuditLog>> getLogsByUser(@PathVariable String userId) {
Page<AuditLog> logs = auditLogRepository.findByPerformedByIdOrderByCreatedAtDesc(userId);
return ResponseEntity.ok(logs);
}
Comment thread
SandraNelj marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.example.team6backend.auditlog.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.example.team6backend.user.entity.AppUser;

import java.time.Instant;

@Entity
@Getter
@Setter
public class AuditLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String userName;
private String action;
private String targetType;
private String targetId;
private String details;
private Instant createdAt;

@ManyToOne
@JoinColumn(name = "user_id")
private AppUser performedBy;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.example.team6backend.auditlog.repository;

import org.example.team6backend.auditlog.entity.AuditLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {

@Query("SELECT a FROM AuditLog a " + "WHERE LOWER(a.performedBy.name) LIKE LOWER(CONCAT('%', :s, '%')) "
+ "OR LOWER(a.action) LIKE LOWER(CONCAT('%', :s, '%'))")
Page<AuditLog> searchLogs(@Param("s") String search, Pageable pageable);

Page<AuditLog> findByPerformedByIdOrderByCreatedAtDesc(String userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.example.team6backend.auditlog.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.team6backend.auditlog.entity.AuditLog;
import org.example.team6backend.auditlog.repository.AuditLogRepository;
import org.example.team6backend.user.entity.AppUser;
import org.example.team6backend.user.repository.AppUserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
private final AppUserRepository appUserRepository;

@Transactional
public void log(String action, String details, AppUser performedBy) {
log(action, details, performedBy, null, null);
}

public void log(String action, String details, AppUser performedBy, String targetType, String targetId) {
try {
AuditLog log = new AuditLog();
log.setUserName(performedBy.getUsername());
log.setAction(action);
log.setTargetType(targetType);
log.setTargetId(targetId);
log.setDetails(details);
log.setCreatedAt(Instant.now());
log.setPerformedBy(performedBy);

auditLogRepository.save(log);
} catch (Exception e) {
log.warn("Failed to write audit log: {}", e.getMessage());
}
}
Comment thread
SandraNelj marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public ResponseEntity<Resource> getFile(@PathVariable String fileKey,
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}

InputStream inputStream = documentService.downloadFile(fileKey);
InputStream inputStream = documentService.downloadFile(fileKey, user);
MediaType mediaType = document.getContentType() != null
? MediaType.parseMediaType(document.getContentType())
: MediaType.APPLICATION_OCTET_STREAM;
Expand All @@ -72,7 +72,7 @@ public ResponseEntity<List<DocumentDTO>> uploadFile(@PathVariable Long incidentI

for (MultipartFile file : files) {
if (!file.isEmpty()) {
Document doc = documentService.uploadFile(file, incident);
Document doc = documentService.uploadFile(file, incident, user);
DocumentDTO dto = new DocumentDTO();
dto.setFileName(doc.getFileName());
dto.setFileKey(doc.getFileKey());
Expand All @@ -93,10 +93,12 @@ public ResponseEntity<Void> deleteFile(@PathVariable Long documentId,
@AuthenticationPrincipal CustomUserDetails userDetails) {

log.info("DELETE /documents/{} - Deleting file", documentId);
AppUser user = userDetails.getUser();

Document document = documentService.getById(documentId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

documentService.deleteFile(document);
documentService.deleteFile(document, user);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.team6backend.auditlog.service.AuditLogService;
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.document.repository.DocumentRepository;
import org.example.team6backend.incident.entity.Incident;
import org.example.team6backend.user.entity.AppUser;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -23,10 +25,11 @@ public class DocumentService {

private final MinioService minioService;
private final DocumentRepository documentRepository;
private final AuditLogService auditLogService;

/** Upload file */
@Transactional
public Document uploadFile(MultipartFile file, Incident incident) {
public Document uploadFile(MultipartFile file, Incident incident, AppUser user) {
String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename();
boolean uploaded = false;

Expand All @@ -41,7 +44,12 @@ public Document uploadFile(MultipartFile file, Incident incident) {
document.setFileSize(file.getSize());
document.setIncident(incident);

return documentRepository.save(document);
Document savedDocument = documentRepository.save(document);

auditLogService.log("UPLOAD_DOCUMENT", user.getName() + " uploaded '" + file.getOriginalFilename() + "'",
user, "Document", savedDocument.getId().toString());

return savedDocument;

} catch (Exception e) {
if (uploaded) {
Expand All @@ -57,9 +65,14 @@ public Document uploadFile(MultipartFile file, Incident incident) {

/** Download file */
@Transactional
public InputStream downloadFile(String objectKey) {
public InputStream downloadFile(String objectKey, AppUser user) {
try {
return minioService.downloadFile(objectKey);
InputStream stream = minioService.downloadFile(objectKey);

auditLogService.log("DOWNLOAD_DOCUMENT", user.getName() + " downloaded file '" + objectKey + "'", user);

return stream;

} catch (MinioService.FileMissingException e) {
log.warn("Missing file in Minio: {}", objectKey, e);

Expand All @@ -82,14 +95,20 @@ public InputStream downloadFile(String objectKey) {

/** Delete file */
@Transactional
public void deleteFile(Document document) {
public void deleteFile(Document document, AppUser user) {
String fileName = document.getFileName();
String fileKey = document.getFileKey();

documentRepository.delete(document);
auditLogService.log("DELETE_DOCUMENT", user.getName() + " deleted file '" + fileName + "'", user, "Document",
document.getId().toString());

try {
minioService.deleteFile(document.getFileKey());
minioService.deleteFile(fileKey);
} catch (Exception e) {
log.warn("Could not delete file: {}", document.getFileKey(), e);
log.warn("Could not delete file: {}", fileKey, e);
}

}
Comment thread
SandraNelj marked this conversation as resolved.

/** Fetch all files connected to one incident */
Expand Down
Loading
Loading