diff --git a/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java b/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java index f63d676..39f7729 100644 --- a/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java +++ b/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java @@ -1,5 +1,7 @@ package org.example.team6backend.activity.controller; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.example.team6backend.activity.dto.ActivityLogResponse; import org.example.team6backend.activity.service.ActivityLogService; import org.springframework.http.ResponseEntity; @@ -12,19 +14,17 @@ @RestController @RequestMapping("/activity") +@RequiredArgsConstructor +@Slf4j public class ActivityLogController { private final ActivityLogService activityLogService; - public ActivityLogController(ActivityLogService activityLogService) { - this.activityLogService = activityLogService; - } - @GetMapping("/incident/{incidentId}") public ResponseEntity> getActivityByIncidentId(@PathVariable Long incidentId) { + log.info("GET /activity/incident/{} - Fetching activity log", incidentId); List activityLogs = activityLogService.getByIncidentId(incidentId).stream() .map(ActivityLogResponse::fromEntity).toList(); - return ResponseEntity.ok(activityLogs); } } diff --git a/src/main/java/org/example/team6backend/comment/controller/CommentController.java b/src/main/java/org/example/team6backend/comment/controller/CommentController.java index 350b4f0..4fb9de6 100644 --- a/src/main/java/org/example/team6backend/comment/controller/CommentController.java +++ b/src/main/java/org/example/team6backend/comment/controller/CommentController.java @@ -1,9 +1,12 @@ package org.example.team6backend.comment.controller; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.example.team6backend.comment.dto.CommentRequest; import org.example.team6backend.comment.dto.CommentResponse; import org.example.team6backend.comment.service.CommentService; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -11,25 +14,25 @@ @RestController @RequestMapping("/comments") +@RequiredArgsConstructor +@Slf4j public class CommentController { private final CommentService commentService; - public CommentController(CommentService commentService) { - this.commentService = commentService; - } - @GetMapping("/incident/{incidentId}") public ResponseEntity> getCommentByIncidentId(@PathVariable Long incidentId) { + log.info("GET /comments/incident/{} - Fetching comments", incidentId); List comments = commentService.getCommentByIncidentId(incidentId).stream() .map(CommentResponse::fromEntity).toList(); - return ResponseEntity.ok(comments); } @PostMapping - public ResponseEntity createComment(@RequestBody @Valid CommentRequest request) { - commentService.createComment(request.getIncidentId(), request.getUserId(), request.getMessage()); - return ResponseEntity.ok().build(); + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createComment(@Valid @RequestBody CommentRequest request) { + log.info("POST /comments - Creating comment for incident: {}", request.getIncidentId()); + var comment = commentService.createComment(request.getIncidentId(), request.getUserId(), request.getMessage()); + return ResponseEntity.status(HttpStatus.CREATED).body(CommentResponse.fromEntity(comment)); } } diff --git a/src/main/java/org/example/team6backend/config/SecurityConfig.java b/src/main/java/org/example/team6backend/config/SecurityConfig.java index 26b96fa..3fbfb02 100644 --- a/src/main/java/org/example/team6backend/config/SecurityConfig.java +++ b/src/main/java/org/example/team6backend/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; + @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) @@ -18,19 +19,23 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests( - auth -> auth.requestMatchers("/", "/index", "/demo", "/error", "/login/**", "/oauth2/**", "/dev/**") - .permitAll().requestMatchers("/dashboard.html", "/profile.html", "/viewincident.html") - .authenticated().requestMatchers("/incidents.html/**", "/api/incidents/**") - .hasAnyRole("RESIDENT", "HANDLER", "ADMIN").requestMatchers("/admin.html", "/api/admin/**") - .hasRole("ADMIN").requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**") - .hasRole("ADMIN").anyRequest().authenticated()) + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/", "/index.html", "/dashboard.html", "/incidents.html", "/profile.html", + "/createincident.html", "/viewincident.html", "/admin.html", "/demo.html", "/error", + "/login/**", "/oauth2/**", "/dev/**", "/css/**", "/js/**", "/images/**") + .permitAll() + + .requestMatchers("/api/incidents/**").hasAnyRole("RESIDENT", "HANDLER", "ADMIN") + .requestMatchers("/api/admin/**").hasRole("ADMIN").requestMatchers("/api/users/me").authenticated() + .requestMatchers("/comments/**", "/activity/**", "/documents/**").authenticated() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").hasRole("ADMIN").anyRequest() + .authenticated()) .oauth2Login( oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) .defaultSuccessUrl("/dashboard.html", true)) .logout(logout -> logout.logoutSuccessUrl("/").invalidateHttpSession(true).clearAuthentication(true) .deleteCookies("JSESSIONID")) - .csrf(csrf -> csrf.ignoringRequestMatchers("/api/admin/**", "/api/incidents/**", "/api/documents/**", + .csrf(csrf -> csrf.ignoringRequestMatchers("/api/admin/**", "/api/incidents/**", "/comments", "/dev/**")); return http.build(); diff --git a/src/main/java/org/example/team6backend/document/controller/DocumentController.java b/src/main/java/org/example/team6backend/document/controller/DocumentController.java index 4fd8fc9..e94cc24 100644 --- a/src/main/java/org/example/team6backend/document/controller/DocumentController.java +++ b/src/main/java/org/example/team6backend/document/controller/DocumentController.java @@ -1,5 +1,8 @@ package org.example.team6backend.document.controller; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.team6backend.document.dto.DocumentDTO; import org.example.team6backend.document.entity.Document; import org.example.team6backend.document.service.DocumentService; import org.example.team6backend.document.service.MinioService; @@ -19,31 +22,24 @@ import org.springframework.web.server.ResponseStatusException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; @RestController -@RequestMapping("/api/documents") +@RequestMapping("/documents") +@RequiredArgsConstructor +@Slf4j public class DocumentController { private final DocumentService documentService; private final IncidentService incidentService; private final MinioService minioService; - public DocumentController(DocumentService documentService, IncidentService incidentService, - MinioService minioService) { - this.documentService = documentService; - this.incidentService = incidentService; - this.minioService = minioService; - } - @GetMapping("/{fileKey}") public ResponseEntity getFile(@PathVariable String fileKey, @AuthenticationPrincipal CustomUserDetails userDetails) { - if (userDetails == null) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); - } - + log.info("GET /documents/{} - Fetching file", fileKey); AppUser user = userDetails.getUser(); Document document = documentService.getByFileKey(fileKey) @@ -55,7 +51,6 @@ public ResponseEntity getFile(@PathVariable String fileKey, } InputStream inputStream = minioService.getFile(fileKey); - MediaType mediaType = document.getContentType() != null ? MediaType.parseMediaType(document.getContentType()) : MediaType.APPLICATION_OCTET_STREAM; @@ -66,33 +61,42 @@ public ResponseEntity getFile(@PathVariable String fileKey, } @PostMapping("/upload/{incidentId}") - public ResponseEntity uploadFile(@PathVariable Long incidentId, + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity> uploadFile(@PathVariable Long incidentId, @RequestParam("files") List files, @AuthenticationPrincipal CustomUserDetails userDetails) { + log.info("POST /documents/upload/{} - Uploading {} files", incidentId, files.size()); AppUser user = userDetails.getUser(); Incident incident = incidentService.getById(incidentId, user); + List uploadedDocs = new ArrayList<>(); for (MultipartFile file : files) { if (!file.isEmpty()) { - documentService.uploadFile(file, incident); + Document doc = documentService.uploadFile(file, incident); + DocumentDTO dto = new DocumentDTO(); + dto.setFileName(doc.getFileName()); + dto.setFileKey(doc.getFileKey()); + dto.setContentType(doc.getContentType()); + dto.setFileSize(doc.getFileSize()); + dto.setImage(doc.getContentType() != null && doc.getContentType().startsWith("image/")); + uploadedDocs.add(dto); + log.debug("Uploaded file: {}", doc.getFileName()); } } - return ResponseEntity.ok().build(); + + return ResponseEntity.status(HttpStatus.CREATED).body(uploadedDocs); } - @GetMapping("/{fileKey}/download") - public ResponseEntity downloadFile(@PathVariable String fileKey, + @DeleteMapping("/{documentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deleteFile(@PathVariable Long documentId, @AuthenticationPrincipal CustomUserDetails userDetails) { - AppUser user = userDetails.getUser(); - Document document = documentService.getByFileKey(fileKey) + log.info("DELETE /documents/{} - Deleting file", documentId); + Document document = documentService.getById(documentId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - InputStream stream = minioService.getFile(fileKey); - - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"") - .contentType(MediaType.parseMediaType(document.getContentType())).body(new InputStreamResource(stream)); - + documentService.deleteFile(document); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java b/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java index ab4d083..0a37071 100644 --- a/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java +++ b/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java @@ -6,6 +6,8 @@ public class DocumentDTO { private String fileName; private String fileKey; - private String fileUrl; + private String contentType; + private Long fileSize; private boolean image; + private String fileUrl; } diff --git a/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java b/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java index 09adff9..96d996f 100644 --- a/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java +++ b/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java @@ -8,6 +8,10 @@ import java.util.Optional; public interface DocumentRepository extends JpaRepository { + List findByIncident(Incident incident); + + List findByIncidentId(Long incidentId); + Optional findByFileKey(String fileKey); } diff --git a/src/main/java/org/example/team6backend/document/service/DocumentService.java b/src/main/java/org/example/team6backend/document/service/DocumentService.java index 3b77be6..eda31f0 100644 --- a/src/main/java/org/example/team6backend/document/service/DocumentService.java +++ b/src/main/java/org/example/team6backend/document/service/DocumentService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; + import java.io.InputStream; import java.util.List; import java.util.Optional; @@ -22,8 +23,8 @@ public class DocumentService { private final DocumentRepository documentRepository; /** Upload file */ + @Transactional public Document uploadFile(MultipartFile file, Incident incident) { - String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename(); boolean uploaded = false; @@ -45,7 +46,7 @@ public Document uploadFile(MultipartFile file, Incident incident) { try { minioService.deleteFile(fileKey); } catch (Exception cleanupEx) { - log.warn("Failed to cleanup S3 file: {}", fileKey, cleanupEx); + log.warn("Failed to cleanup Minio file: {}", fileKey, cleanupEx); } } throw new RuntimeException("File upload failed", e); @@ -60,19 +61,32 @@ public InputStream downloadFile(String objectKey) { /** Delete file */ @Transactional public void deleteFile(Document document) { + documentRepository.delete(document); + try { - documentRepository.delete(document); minioService.deleteFile(document.getFileKey()); - } catch (RuntimeException e) { + } catch (Exception e) { log.warn("Could not delete file: {}", document.getFileKey(), e); } } /** Fetch all files connected to one incident */ - public List getDocumentsByIncident(Incident incidentId) { - return documentRepository.findByIncident(incidentId); + public List getDocumentsByIncident(Incident incident) { + return documentRepository.findByIncident(incident); } + + /** Fetch document by fileKey */ public Optional getByFileKey(String fileKey) { return documentRepository.findByFileKey(fileKey); } + + /** Fetch document by ID */ + public Optional getById(Long documentId) { + return documentRepository.findById(documentId); + } + + /** Fetch documents by incident ID */ + public List getDocumentsByIncidentId(Long incidentId) { + return documentRepository.findByIncidentId(incidentId); + } } 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 d8ea623..0bea6be 100644 --- a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java +++ b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java @@ -1,11 +1,14 @@ package org.example.team6backend.incident.controller; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.example.team6backend.incident.dto.AssignIncidentRequest; import org.example.team6backend.incident.dto.IncidentRequest; import org.example.team6backend.incident.dto.IncidentResponse; 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.service.IncidentService; import org.example.team6backend.notification.service.NotificationService; import org.example.team6backend.security.CustomUserDetails; @@ -17,18 +20,22 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; 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 org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; import java.util.List; @RestController @RequestMapping("/api/incidents") +@RequiredArgsConstructor +@Slf4j public class IncidentController { private final IncidentService incidentService; @@ -36,56 +43,70 @@ public class IncidentController { private final UserMapper userMapper; private final NotificationService notificationService; - public IncidentController(IncidentService incidentService, UserService userService, UserMapper userMapper, - NotificationService notificationService) { - this.incidentService = incidentService; - this.userService = userService; - this.userMapper = userMapper; - this.notificationService = notificationService; + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')") + public ResponseEntity createIncidentWithFiles(@RequestParam("subject") String subject, + @RequestParam(value = "description", required = false) String description, + @RequestParam("incidentCategory") IncidentCategory incidentCategory, + @RequestParam(value = "files", required = false) List files, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + log.info("POST /api/incidents - Creating new incident with {} files", files != null ? files.size() : 0); + AppUser user = customUserDetails.getUser(); + + IncidentRequest incidentRequest = new IncidentRequest(); + incidentRequest.setSubject(subject); + incidentRequest.setDescription(description); + incidentRequest.setIncidentCategory(incidentCategory); + + Incident saved = incidentService.createIncident(incidentRequest, files, user); + return ResponseEntity.status(HttpStatus.CREATED).body(IncidentResponse.fromEntityBasic(saved)); } - /** Create new incident */ - @PostMapping + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')") - public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest, + public ResponseEntity createIncident(@RequestBody @Valid IncidentRequest incidentRequest, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - AppUser user = customUserDetails.getUser(); + log.info("POST /api/incidents (JSON) - Creating new incident"); + AppUser user = customUserDetails.getUser(); Incident saved = incidentService.createIncident(incidentRequest, null, user); - - return IncidentResponse.fromEntityBasic(saved); + return ResponseEntity.status(HttpStatus.CREATED).body(IncidentResponse.fromEntityBasic(saved)); } - /** Get my incidents(user) */ @PreAuthorize("hasRole('RESIDENT')") @GetMapping("/my") - public Page getMyIncidents(@AuthenticationPrincipal CustomUserDetails userDetails, + public ResponseEntity> getMyIncidents(@AuthenticationPrincipal CustomUserDetails userDetails, Pageable pageable) { - + log.info("GET /api/incidents/my - Fetching my incidents"); AppUser user = userDetails.getUser(); - return incidentService.findByCreatedBy(user, pageable).map(IncidentResponse::fromEntityBasic); + return ResponseEntity + .ok(incidentService.findByCreatedBy(user, pageable).map(IncidentResponse::fromEntityBasic)); } - /** Get assigned incidents(handler) */ @PreAuthorize("hasRole('HANDLER')") @GetMapping("/assigned") - public Page getAssignedIncidents(@AuthenticationPrincipal CustomUserDetails userDetails, - Pageable pageable) { + public ResponseEntity> getAssignedIncidents( + @AuthenticationPrincipal CustomUserDetails userDetails, Pageable pageable) { + log.info("GET /api/incidents/assigned - Fetching assigned incidents"); AppUser user = userDetails.getUser(); - return incidentService.findByAssignedTo(user, pageable).map(IncidentResponse::fromEntityBasic); + return ResponseEntity + .ok(incidentService.findByAssignedTo(user, pageable).map(IncidentResponse::fromEntityBasic)); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/all") - public Page getAllIncidents(Pageable pageable) { - return incidentService.findAll(pageable).map(IncidentResponse::fromEntityBasic); + public ResponseEntity> getAllIncidents(Pageable pageable) { + log.info("GET /api/incidents/all - Fetching all incidents"); + return ResponseEntity.ok(incidentService.findAll(pageable).map(IncidentResponse::fromEntityBasic)); } @PreAuthorize("hasAnyRole('RESIDENT', 'HANDLER', 'ADMIN')") @GetMapping("/{id}") - public IncidentResponse getIncidentById(@PathVariable Long id) { - + public ResponseEntity getIncidentById(@PathVariable Long id) { + log.info("GET /api/incidents/{} - Fetching incident", id); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails userDetails)) { @@ -93,25 +114,26 @@ public IncidentResponse getIncidentById(@PathVariable Long id) { } AppUser currentUser = userDetails.getUser(); - notificationService.markNotificationAsReadForIncident(currentUser.getId(), id); - return IncidentResponse.fromEntityWithDocuments(incidentService.getById(id, currentUser)); + return ResponseEntity.ok(IncidentResponse.fromEntityWithDocuments(incidentService.getById(id, currentUser))); } @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{incidentId}/assign") - public IncidentResponse assignIncident(@PathVariable Long incidentId, + public ResponseEntity assignIncident(@PathVariable Long incidentId, @Valid @RequestBody AssignIncidentRequest request, @AuthenticationPrincipal CustomUserDetails adminUser) { + log.info("PATCH /api/incidents/{}/assign - Assigning to handler {}", incidentId, request.handlerId()); Incident updatedIncident = incidentService.assignIncidentToHandler(incidentId, request.handlerId(), adminUser.getUser()); - return IncidentResponse.fromEntityBasic(updatedIncident); + return ResponseEntity.ok(IncidentResponse.fromEntityBasic(updatedIncident)); } @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{incidentId}/unassign") public ResponseEntity unassignIncident(@PathVariable Long incidentId, @AuthenticationPrincipal CustomUserDetails adminUser) { + log.info("PATCH /api/incidents/{}/unassign - Unassigning incident", incidentId); Incident updatedIncident = incidentService.unassignIncident(incidentId, adminUser.getUser()); return ResponseEntity.ok(IncidentResponse.fromEntityBasic(updatedIncident)); } @@ -120,6 +142,7 @@ public ResponseEntity unassignIncident(@PathVariable Long inci @PatchMapping("/{incidentId}/close") public ResponseEntity closeIncident(@PathVariable Long incidentId, @AuthenticationPrincipal CustomUserDetails userDetails) { + log.info("PATCH /api/incidents/{}/close - Closing incident", incidentId); Incident closedIncident = incidentService.closeIncident(incidentId, userDetails.getUser()); return ResponseEntity.ok(IncidentResponse.fromEntityBasic(closedIncident)); } @@ -128,25 +151,26 @@ public ResponseEntity closeIncident(@PathVariable Long inciden @PatchMapping("/{incidentId}/resolve") public ResponseEntity resolveIncident(@PathVariable Long incidentId, @AuthenticationPrincipal CustomUserDetails userDetails) { + log.info("PATCH /api/incidents/{}/resolve - Resolving incident", incidentId); Incident resolvedIncident = incidentService.resolveIncident(incidentId, userDetails.getUser()); return ResponseEntity.ok(IncidentResponse.fromEntityBasic(resolvedIncident)); } @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{incidentId}/status") - public IncidentResponse updateStatus(@PathVariable Long incidentId, + public ResponseEntity updateStatus(@PathVariable Long incidentId, @Valid @RequestBody UpdateIncidentStatusRequest request, @AuthenticationPrincipal CustomUserDetails adminUser) { - - Incident updateIncident = incidentService.updateIncidentStatus(incidentId, request.status(), + log.info("PATCH /api/incidents/{}/status - Updating status to {}", incidentId, request.status()); + Incident updatedIncident = incidentService.updateIncidentStatus(incidentId, request.status(), adminUser.getUser()); - - return IncidentResponse.fromEntity(updateIncident); + return ResponseEntity.ok(IncidentResponse.fromEntity(updatedIncident)); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/handlers") public ResponseEntity> getAvailableHandlers() { + log.info("GET /api/incidents/handlers - Fetching all handlers"); List handlers = userService.getUsersByRole(UserRole.HANDLER); return ResponseEntity.ok(handlers.stream().map(userMapper::toResponse).toList()); } diff --git a/src/main/java/org/example/team6backend/incident/dto/IncidentRequest.java b/src/main/java/org/example/team6backend/incident/dto/IncidentRequest.java index a00eac9..c70877a 100644 --- a/src/main/java/org/example/team6backend/incident/dto/IncidentRequest.java +++ b/src/main/java/org/example/team6backend/incident/dto/IncidentRequest.java @@ -7,10 +7,11 @@ @Data public class IncidentRequest { - @NotBlank(message = "Subject is required") private String subject; + private String description; + @NotNull(message = "Incident category is required") private IncidentCategory incidentCategory; } diff --git a/src/main/java/org/example/team6backend/notification/controller/NotificationController.java b/src/main/java/org/example/team6backend/notification/controller/NotificationController.java index e70b6f6..9990efc 100644 --- a/src/main/java/org/example/team6backend/notification/controller/NotificationController.java +++ b/src/main/java/org/example/team6backend/notification/controller/NotificationController.java @@ -1,8 +1,11 @@ package org.example.team6backend.notification.controller; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.example.team6backend.notification.dto.NotificationResponse; import org.example.team6backend.notification.service.NotificationService; import org.example.team6backend.security.CustomUserDetails; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -10,32 +13,34 @@ @RestController @RequestMapping("/notifications") +@RequiredArgsConstructor +@Slf4j public class NotificationController { private final NotificationService notificationService; - public NotificationController(NotificationService notificationService) { - this.notificationService = notificationService; - } - @GetMapping("/user") - public List getUserNotifications(@AuthenticationPrincipal CustomUserDetails userDetails) { - + public ResponseEntity> getUserNotifications( + @AuthenticationPrincipal CustomUserDetails userDetails) { String userId = userDetails.getUser().getId(); - - return notificationService.getUnreadNotifications(userId).stream().map(NotificationResponse::fromEntity) - .toList(); + log.info("GET /notifications/user - Fetching notifications for user: {}", userId); + return ResponseEntity.ok(notificationService.getUnreadNotifications(userId).stream() + .map(NotificationResponse::fromEntity).toList()); } @GetMapping("/user/unread-count") - public long getUnreadCount(@AuthenticationPrincipal CustomUserDetails userDetails) { + public ResponseEntity getUnreadCount(@AuthenticationPrincipal CustomUserDetails userDetails) { String userId = userDetails.getUser().getId(); - return notificationService.getUnreadCount(userId); + log.info("GET /notifications/user/unread-count - Fetching unread count for user: {}", userId); + return ResponseEntity.ok(notificationService.getUnreadCount(userId)); } - @PatchMapping(("/{notificationId}/read")) - public void markAsRead(@PathVariable Long notificationId, @AuthenticationPrincipal CustomUserDetails userDetails) { + @PatchMapping("/{notificationId}/read") + public ResponseEntity markAsRead(@PathVariable Long notificationId, + @AuthenticationPrincipal CustomUserDetails userDetails) { String userId = userDetails.getUser().getId(); + log.info("PATCH /notifications/{}/read - Marking notification as read", notificationId); notificationService.markAsRead(notificationId, userId); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/org/example/team6backend/user/service/UserService.java b/src/main/java/org/example/team6backend/user/service/UserService.java index d751771..9c0405c 100644 --- a/src/main/java/org/example/team6backend/user/service/UserService.java +++ b/src/main/java/org/example/team6backend/user/service/UserService.java @@ -1,5 +1,6 @@ package org.example.team6backend.user.service; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.team6backend.user.entity.AppUser; @@ -21,6 +22,7 @@ public class UserService { private final AppUserRepository userRepository; + private final EntityManager entityManager; @Transactional public AppUser createOrUpdateUser(Map attributes) { @@ -125,8 +127,36 @@ public void deleteUser(String userId) { } } + int assignedUpdated = entityManager + .createNativeQuery("UPDATE incident SET assigned_to_id = NULL WHERE assigned_to_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Updated {} incidents where user was assigned", assignedUpdated); + + int createdUpdated = entityManager + .createNativeQuery("UPDATE incident SET created_by_id = NULL WHERE created_by_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Updated {} incidents where user was creator", createdUpdated); + + int modifiedUpdated = entityManager + .createNativeQuery("UPDATE incident SET modified_by_id = NULL WHERE modified_by_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Updated {} incidents where user was modifier", modifiedUpdated); + + int commentsUpdated = entityManager + .createNativeQuery("UPDATE comment SET user_id = NULL WHERE user_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Updated {} comments", commentsUpdated); + + int activityDeleted = entityManager.createNativeQuery("DELETE FROM activity_log WHERE user_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Deleted {} activity log entries", activityDeleted); + + int notificationsDeleted = entityManager.createNativeQuery("DELETE FROM notification WHERE user_id = :userId") + .setParameter("userId", userId).executeUpdate(); + log.debug("Deleted {} notifications", notificationsDeleted); + userRepository.delete(user); - log.info("Deleted user: userId={}, githubLogin={}", userId, user.getGithubLogin()); + log.info("Deleted user: userId={}, githubLogin={}, role={}", userId, user.getGithubLogin(), user.getRole()); } private AppUser updateExistingUser(AppUser existingUser, OAuthUserInfo oauthUserInfo) { diff --git a/src/main/resources/static/admin.html b/src/main/resources/static/admin.html index a360c5f..4cc157c 100644 --- a/src/main/resources/static/admin.html +++ b/src/main/resources/static/admin.html @@ -1,15 +1,11 @@ - + Admin Panel - Incident Manager @@ -771,13 +566,13 @@ Incident Management System @@ -790,30 +585,12 @@

Admin Panel

-
-
Total Users
-
-
-
-
-
Pending Approval
-
-
-
-
-
Residents
-
-
-
-
-
Handlers
-
-
-
-
-
Admins
-
-
-
-
-
Total Incidents
-
-
-
+
Total Users
-
+
Pending Approval
-
+
Residents
-
+
Handlers
-
+
Admins
-
+
Total Incidents
-
@@ -824,7 +601,7 @@

Admin Panel

@@ -849,31 +626,19 @@

Admin Panel

- - - - - - - - + - - - +
UserEmail / GitHubRoleStatusCreatedActions
UserEmail / GitHubRoleStatusCreatedActions
Loading users...
Loading users...
-
- Showing 0-0 of 0 users -
-
-
+
Showing 0-0 of 0 users
+
- + @@ -987,10 +738,6 @@

Assign Incident to Handler

-
- - -
+ ${incident.incidentCategory} + + ${escapeHtml(incident.createdBy || 'Unknown')} + ${incident.assignedTo ? `✓ ${escapeHtml(incident.assignedTo)}` : 'Unassigned'} + ${formatDate(incident.createdAt)} +
${actionButton}
- `; + `; }).join(''); } function renderIncidentPagination() { - const paginationDiv = document.getElementById('incidentPaginationControls'); - + const div = document.getElementById('incidentPaginationControls'); if (incidentTotalPages <= 1) { - paginationDiv.innerHTML = ''; + div.innerHTML = ''; return; } - let html = ''; html += ``; html += ``; - - const maxVisiblePages = 5; - let startPage = Math.max(0, currentIncidentPage - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(incidentTotalPages - 1, startPage + maxVisiblePages - 1); - - if (endPage - startPage + 1 < maxVisiblePages) { - startPage = Math.max(0, endPage - maxVisiblePages + 1); - } - - if (startPage > 0) { - html += ``; - if (startPage > 1) { - html += `...`; - } - } - + let startPage = Math.max(0, currentIncidentPage - 2); + let endPage = Math.min(incidentTotalPages - 1, startPage + 4); for (let i = startPage; i <= endPage; i++) { html += ``; } - - if (endPage < incidentTotalPages - 1) { - if (endPage < incidentTotalPages - 2) { - html += `...`; - } - html += ``; - } - html += ``; html += ``; - - paginationDiv.innerHTML = html; + div.innerHTML = html; } function updateIncidentPaginationInfo() { @@ -1433,13 +1066,12 @@

Assign Incident to Handler

loadIncidents(); } - document.getElementById('statusFilterIncident')?.addEventListener('change', (e) => { + document.getElementById('statusFilterIncident')?.addEventListener('change', e => { currentIncidentStatus = e.target.value; currentIncidentPage = 0; loadIncidents(); }); - - document.getElementById('incidentSearchInput')?.addEventListener('keypress', (e) => { + document.getElementById('incidentSearchInput')?.addEventListener('keypress', e => { if (e.key === 'Enter') searchIncidents(); }); @@ -1456,69 +1088,61 @@

Assign Incident to Handler

async function saveAssignment() { const handlerId = document.getElementById('handlerSelect').value; - if (!handlerId) { showToast('Please select a handler', 'error'); return; } - try { - const response = await fetch(`/api/incidents/${selectedIncidentId}/assign`, { + const r = await fetch(`/api/incidents/${selectedIncidentId}/assign`, { method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({handlerId: handlerId}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ handlerId: handlerId }) }); - - if (response.ok) { + if (r.ok) { showToast('Incident assigned successfully!', 'success'); closeAssignModal(); loadIncidents(); loadStats(); } else { - const error = await response.text(); - showToast('Failed to assign incident: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to assign incident: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error assigning incident', 'error'); } } async function updateIncidentStatus(incidentId, newStatus) { try { - const response = await fetch(`/api/incidents/${incidentId}/status`, { - method: "PATCH", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({status: newStatus}) + const r = await fetch(`/api/incidents/${incidentId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) }); - - if (response.ok) { - showToast('Status updated!', 'success') + if (r.ok) { + showToast('Status updated!', 'success'); loadIncidents(); } else { - const error = await response.text(); - showToast('Failed to update incident status: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to update incident status: ' + e, 'error'); } - } catch (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 response = await fetch(`/api/incidents/${incidentId}/unassign`, { - method: 'PATCH' - }); - - if (response.ok) { + const r = await fetch(`/api/incidents/${incidentId}/unassign`, { method: 'PATCH' }); + if (r.ok) { showToast('Incident unassigned successfully!', 'success'); loadIncidents(); } else { - const error = await response.text(); - showToast('Failed to unassign incident: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to unassign incident: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error unassigning incident', 'error'); } } @@ -1526,24 +1150,21 @@

Assign Incident to Handler

function closeAssignModal() { document.getElementById('assignModal').classList.remove('active'); selectedIncidentId = null; - document.getElementById('assignmentNotes').value = ''; } async function approveUser(userId) { if (!confirm('Approve this user? They will get RESIDENT role.')) return; - try { - const response = await fetch(`/api/admin/users/${userId}/approve`, {method: 'POST'}); - - if (response.ok) { + const r = await fetch(`/api/admin/users/${userId}/approve`, { method: 'POST' }); + if (r.ok) { showToast('User approved successfully!', 'success'); loadStats(); loadUsers(); } else { - const error = await response.text(); - showToast('Failed to approve user: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to approve user: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error approving user', 'error'); } } @@ -1555,24 +1176,22 @@

Assign Incident to Handler

async function saveRoleChange() { const newRole = document.getElementById('newRoleSelect').value; - try { - const response = await fetch(`/api/admin/users/${selectedUserId}/role`, { + const r = await fetch(`/api/admin/users/${selectedUserId}/role`, { method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({role: newRole}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ role: newRole }) }); - - if (response.ok) { + if (r.ok) { showToast('Role updated successfully!', 'success'); closeRoleModal(); loadStats(); loadUsers(); } else { - const error = await response.text(); - showToast('Failed to update role: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to update role: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error updating role', 'error'); } } @@ -1584,31 +1203,29 @@

Assign Incident to Handler

function openStatusModal(userId, currentActive) { selectedUserId = userId; - const select = document.getElementById('newStatusSelect'); - select.value = currentActive ? 'false' : 'true'; + const s = document.getElementById('newStatusSelect'); + s.value = currentActive ? 'false' : 'true'; document.getElementById('statusModal').classList.add('active'); } async function saveStatusChange() { const newStatus = document.getElementById('newStatusSelect').value === 'true'; - try { - const response = await fetch(`/api/admin/users/${selectedUserId}/status`, { + const r = await fetch(`/api/admin/users/${selectedUserId}/status`, { method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({active: newStatus}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ active: newStatus }) }); - - if (response.ok) { + if (r.ok) { showToast('Status updated successfully!', 'success'); closeStatusModal(); loadStats(); loadUsers(); } else { - const error = await response.text(); - showToast('Failed to update status: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to update status: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error updating status', 'error'); } } @@ -1619,48 +1236,45 @@

Assign Incident to Handler

} async function deleteUser(userId) { - if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) return; - + if (!confirm('Delete this user? This action cannot be undone.')) return; try { - const response = await fetch(`/api/admin/users/${userId}`, {method: 'DELETE'}); - - if (response.ok) { + const r = await fetch(`/api/admin/users/${userId}`, { method: 'DELETE' }); + if (r.ok) { showToast('User deleted successfully!', 'success'); loadStats(); loadUsers(); } else { - const error = await response.text(); - showToast('Failed to delete user: ' + error, 'error'); + const e = await r.text(); + showToast('Failed to delete user: ' + e, 'error'); } - } catch (error) { + } catch(e) { showToast('Error deleting user', 'error'); } } async function logout() { - await fetch('/logout', {method: 'POST'}); + await fetch('/logout', { method: 'POST' }); window.location.href = '/'; } - function showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - toast.textContent = message; - document.body.appendChild(toast); - setTimeout(() => toast.remove(), 3000); + function showToast(message, type) { + const t = document.createElement('div'); + t.className = `toast ${type}`; + t.textContent = message; + document.body.appendChild(t); + setTimeout(() => t.remove(), 3000); } function escapeHtml(text) { if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const d = document.createElement('div'); + d.textContent = text; + return d.innerHTML; } - function formatDate(dateString) { - if (!dateString) return "-"; - - return new Date(dateString).toLocaleString("sv-SE", { + function formatDate(ds) { + if (!ds) return "-"; + return new Date(ds).toLocaleString("sv-SE", { year: "numeric", month: "2-digit", day: "2-digit", diff --git a/src/main/resources/static/createincident.html b/src/main/resources/static/createincident.html index 2262fb0..a9e4c8b 100644 --- a/src/main/resources/static/createincident.html +++ b/src/main/resources/static/createincident.html @@ -1,15 +1,94 @@ - + - Report Incident - + Report Incident - Incident Manager -
+ - Back to dashboard +
+ ← Back to dashboard
-

Report Incident

- -
+

Report New Incident

- - - -
- Please correct the errors below. + +
+ + +
- - - -
+
+ +
- - - -
+
+ + +
- - - - - - -
+
+ + +
- + - -
+
+ + \ No newline at end of file diff --git a/src/main/resources/static/dashboard.html b/src/main/resources/static/dashboard.html index c0c7ebf..47ed4c8 100644 --- a/src/main/resources/static/dashboard.html +++ b/src/main/resources/static/dashboard.html @@ -1,201 +1,37 @@ - + Dashboard - Incident Manager - +