From 13d5fadf5bffe19763be9d47546b1979e9ede033 Mon Sep 17 00:00:00 2001 From: Kristina M Date: Thu, 23 Apr 2026 13:05:05 +0200 Subject: [PATCH 1/2] refactor: rest-api-fetch-js --- .../controller/ActivityLogController.java | 27 +- .../comment/controller/CommentController.java | 44 +- .../team6backend/config/SecurityConfig.java | 54 +- .../controller/DocumentController.java | 150 +-- .../document/dto/DocumentDTO.java | 12 +- .../repository/DocumentRepository.java | 10 +- .../document/service/DocumentService.java | 130 +-- .../controller/IncidentController.java | 269 ++--- .../incident/dto/IncidentRequest.java | 13 +- .../controller/NotificationController.java | 60 +- .../user/service/UserService.java | 386 +++---- src/main/resources/static/admin.html | 960 ++++++------------ src/main/resources/static/createincident.html | 403 ++++++-- src/main/resources/static/dashboard.html | 956 ++--------------- src/main/resources/static/incidents.html | 436 ++------ src/main/resources/static/index.html | 217 +--- src/main/resources/static/profile.html | 292 ++---- src/main/resources/static/viewincident.html | 493 ++------- .../NotificationControllerTest.java | 98 +- 19 files changed, 1724 insertions(+), 3286 deletions(-) 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..baa18f0 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,18 @@ @RestController @RequestMapping("/activity") +@RequiredArgsConstructor +@Slf4j public class ActivityLogController { - private final ActivityLogService activityLogService; + private final ActivityLogService activityLogService; - public ActivityLogController(ActivityLogService activityLogService) { - this.activityLogService = activityLogService; - } - - @GetMapping("/incident/{incidentId}") - public ResponseEntity> getActivityByIncidentId(@PathVariable Long incidentId) { - List activityLogs = activityLogService.getByIncidentId(incidentId).stream() - .map(ActivityLogResponse::fromEntity).toList(); - - return ResponseEntity.ok(activityLogs); - } -} + @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); + } +} \ No newline at end of file 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..323448d 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,26 @@ @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) { - 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(); - } -} + private final 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 + @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)); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/team6backend/config/SecurityConfig.java b/src/main/java/org/example/team6backend/config/SecurityConfig.java index 26b96fa..c275195 100644 --- a/src/main/java/org/example/team6backend/config/SecurityConfig.java +++ b/src/main/java/org/example/team6backend/config/SecurityConfig.java @@ -8,31 +8,45 @@ 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) @RequiredArgsConstructor public class SecurityConfig { - private final CustomOAuth2UserService customOAuth2UserService; + private final CustomOAuth2UserService customOAuth2UserService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + 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() - @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()) - .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/**", - "/dev/**")); + .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/**", "/comments", "/dev/**") + ); - return http.build(); - } -} + return http.build(); + } +} \ No newline at end of file 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..dc1b590 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,80 +22,83 @@ 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); - } - - AppUser user = userDetails.getUser(); - - Document document = documentService.getByFileKey(fileKey) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - - Incident incident = incidentService.getById(document.getIncident().getId(), user); - if (incident == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND); - } - - InputStream inputStream = minioService.getFile(fileKey); - - MediaType mediaType = document.getContentType() != null - ? MediaType.parseMediaType(document.getContentType()) - : MediaType.APPLICATION_OCTET_STREAM; - - return ResponseEntity.ok().contentType(mediaType) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + document.getFileName() + "\"") - .body(new InputStreamResource(inputStream)); - } - - @PostMapping("/upload/{incidentId}") - public ResponseEntity uploadFile(@PathVariable Long incidentId, - @RequestParam("files") List files, @AuthenticationPrincipal CustomUserDetails userDetails) { - - AppUser user = userDetails.getUser(); - Incident incident = incidentService.getById(incidentId, user); - - for (MultipartFile file : files) { - if (!file.isEmpty()) { - documentService.uploadFile(file, incident); - } - } - return ResponseEntity.ok().build(); - } - - @GetMapping("/{fileKey}/download") - public ResponseEntity downloadFile(@PathVariable String fileKey, - @AuthenticationPrincipal CustomUserDetails userDetails) { - AppUser user = userDetails.getUser(); - - Document document = documentService.getByFileKey(fileKey) - .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)); - - } -} + private final DocumentService documentService; + private final IncidentService incidentService; + private final MinioService minioService; + + @GetMapping("/{fileKey}") + public ResponseEntity getFile(@PathVariable String fileKey, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + log.info("GET /documents/{} - Fetching file", fileKey); + AppUser user = userDetails.getUser(); + + Document document = documentService.getByFileKey(fileKey) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + Incident incident = incidentService.getById(document.getIncident().getId(), user); + if (incident == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + + InputStream inputStream = minioService.getFile(fileKey); + MediaType mediaType = document.getContentType() != null + ? MediaType.parseMediaType(document.getContentType()) + : MediaType.APPLICATION_OCTET_STREAM; + + return ResponseEntity.ok() + .contentType(mediaType) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + document.getFileName() + "\"") + .body(new InputStreamResource(inputStream)); + } + + @PostMapping("/upload/{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()) { + 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.status(HttpStatus.CREATED).body(uploadedDocs); + } + + @DeleteMapping("/{documentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deleteFile(@PathVariable Long documentId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + log.info("DELETE /documents/{} - Deleting file", documentId); + Document document = documentService.getById(documentId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + documentService.deleteFile(document); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file 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..4998618 100644 --- a/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java +++ b/src/main/java/org/example/team6backend/document/dto/DocumentDTO.java @@ -4,8 +4,10 @@ @Data public class DocumentDTO { - private String fileName; - private String fileKey; - private String fileUrl; - private boolean image; -} + private String fileName; + private String fileKey; + private String contentType; + private Long fileSize; + private boolean image; + private String fileUrl; +} \ No newline at end of file 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..bc2c41b 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); - Optional findByFileKey(String fileKey); -} + + List findByIncident(Incident incident); + + List findByIncidentId(Long incidentId); + + Optional findByFileKey(String fileKey); +} \ No newline at end of file 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..f0fcb4d 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; @@ -18,61 +19,74 @@ @RequiredArgsConstructor public class DocumentService { - private final MinioService minioService; - private final DocumentRepository documentRepository; - - /** Upload file */ - public Document uploadFile(MultipartFile file, Incident incident) { - - String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename(); - boolean uploaded = false; - - try { - minioService.uploadFile(fileKey, file); - uploaded = true; - - Document document = new Document(); - document.setFileName(file.getOriginalFilename()); - document.setContentType(file.getContentType()); - document.setFileKey(fileKey); - document.setFileSize(file.getSize()); - document.setIncident(incident); - - return documentRepository.save(document); - - } catch (Exception e) { - if (uploaded) { - try { - minioService.deleteFile(fileKey); - } catch (Exception cleanupEx) { - log.warn("Failed to cleanup S3 file: {}", fileKey, cleanupEx); - } - } - throw new RuntimeException("File upload failed", e); - } - } - - /** Download file */ - public InputStream downloadFile(String objectKey) { - return minioService.downloadFile(objectKey); - } - - /** Delete file */ - @Transactional - public void deleteFile(Document document) { - try { - documentRepository.delete(document); - minioService.deleteFile(document.getFileKey()); - } catch (RuntimeException 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 Optional getByFileKey(String fileKey) { - return documentRepository.findByFileKey(fileKey); - } -} + private final MinioService minioService; + private final DocumentRepository documentRepository; + + /** Upload file */ + @Transactional + public Document uploadFile(MultipartFile file, Incident incident) { + String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename(); + boolean uploaded = false; + + try { + minioService.uploadFile(fileKey, file); + uploaded = true; + + Document document = new Document(); + document.setFileName(file.getOriginalFilename()); + document.setContentType(file.getContentType()); + document.setFileKey(fileKey); + document.setFileSize(file.getSize()); + document.setIncident(incident); + + return documentRepository.save(document); + + } catch (Exception e) { + if (uploaded) { + try { + minioService.deleteFile(fileKey); + } catch (Exception cleanupEx) { + log.warn("Failed to cleanup Minio file: {}", fileKey, cleanupEx); + } + } + throw new RuntimeException("File upload failed", e); + } + } + + /** Download file */ + public InputStream downloadFile(String objectKey) { + return minioService.downloadFile(objectKey); + } + + /** Delete file */ + @Transactional + public void deleteFile(Document document) { + documentRepository.delete(document); + + try { + minioService.deleteFile(document.getFileKey()); + } catch (Exception e) { + log.warn("Could not delete file: {}", document.getFileKey(), e); + } + } + + /** Fetch all files connected to one incident */ + 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); + } +} \ No newline at end of file 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..6a14fe9 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,137 +20,163 @@ 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; - private final UserService userService; - 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; - } - - /** Create new incident */ - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - @PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')") - public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { - AppUser user = customUserDetails.getUser(); - - Incident saved = incidentService.createIncident(incidentRequest, null, user); - - return IncidentResponse.fromEntityBasic(saved); - } - - /** Get my incidents(user) */ - @PreAuthorize("hasRole('RESIDENT')") - @GetMapping("/my") - public Page getMyIncidents(@AuthenticationPrincipal CustomUserDetails userDetails, - Pageable pageable) { - - AppUser user = userDetails.getUser(); - return incidentService.findByCreatedBy(user, pageable).map(IncidentResponse::fromEntityBasic); - } - - /** Get assigned incidents(handler) */ - @PreAuthorize("hasRole('HANDLER')") - @GetMapping("/assigned") - public Page getAssignedIncidents(@AuthenticationPrincipal CustomUserDetails userDetails, - Pageable pageable) { - AppUser user = userDetails.getUser(); - return incidentService.findByAssignedTo(user, pageable).map(IncidentResponse::fromEntityBasic); - } - - @PreAuthorize("hasRole('ADMIN')") - @GetMapping("/all") - public Page getAllIncidents(Pageable pageable) { - return incidentService.findAll(pageable).map(IncidentResponse::fromEntityBasic); - } - - @PreAuthorize("hasAnyRole('RESIDENT', 'HANDLER', 'ADMIN')") - @GetMapping("/{id}") - public IncidentResponse getIncidentById(@PathVariable Long id) { - - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails userDetails)) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); - } - - AppUser currentUser = userDetails.getUser(); - - notificationService.markNotificationAsReadForIncident(currentUser.getId(), id); - - return IncidentResponse.fromEntityWithDocuments(incidentService.getById(id, currentUser)); - } - - @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/{incidentId}/assign") - public IncidentResponse assignIncident(@PathVariable Long incidentId, - @Valid @RequestBody AssignIncidentRequest request, @AuthenticationPrincipal CustomUserDetails adminUser) { - Incident updatedIncident = incidentService.assignIncidentToHandler(incidentId, request.handlerId(), - adminUser.getUser()); - return IncidentResponse.fromEntityBasic(updatedIncident); - } - - @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/{incidentId}/unassign") - public ResponseEntity unassignIncident(@PathVariable Long incidentId, - @AuthenticationPrincipal CustomUserDetails adminUser) { - Incident updatedIncident = incidentService.unassignIncident(incidentId, adminUser.getUser()); - return ResponseEntity.ok(IncidentResponse.fromEntityBasic(updatedIncident)); - } - - @PreAuthorize("hasAnyRole('ADMIN', 'HANDLER')") - @PatchMapping("/{incidentId}/close") - public ResponseEntity closeIncident(@PathVariable Long incidentId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - Incident closedIncident = incidentService.closeIncident(incidentId, userDetails.getUser()); - return ResponseEntity.ok(IncidentResponse.fromEntityBasic(closedIncident)); - } - - @PreAuthorize("hasAnyRole('ADMIN', 'HANDLER')") - @PatchMapping("/{incidentId}/resolve") - public ResponseEntity resolveIncident(@PathVariable Long incidentId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - 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, - @Valid @RequestBody UpdateIncidentStatusRequest request, - @AuthenticationPrincipal CustomUserDetails adminUser) { - - Incident updateIncident = incidentService.updateIncidentStatus(incidentId, request.status(), - adminUser.getUser()); - - return IncidentResponse.fromEntity(updateIncident); - } - - @PreAuthorize("hasRole('ADMIN')") - @GetMapping("/handlers") - public ResponseEntity> getAvailableHandlers() { - List handlers = userService.getUsersByRole(UserRole.HANDLER); - return ResponseEntity.ok(handlers.stream().map(userMapper::toResponse).toList()); - } -} + private final IncidentService incidentService; + private final UserService userService; + private final UserMapper userMapper; + private final 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)); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')") + public ResponseEntity createIncident( + @RequestBody @Valid IncidentRequest incidentRequest, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + log.info("POST /api/incidents (JSON) - Creating new incident"); + AppUser user = customUserDetails.getUser(); + Incident saved = incidentService.createIncident(incidentRequest, null, user); + return ResponseEntity.status(HttpStatus.CREATED).body(IncidentResponse.fromEntityBasic(saved)); + } + + @PreAuthorize("hasRole('RESIDENT')") + @GetMapping("/my") + public ResponseEntity> getMyIncidents( + @AuthenticationPrincipal CustomUserDetails userDetails, Pageable pageable) { + log.info("GET /api/incidents/my - Fetching my incidents"); + AppUser user = userDetails.getUser(); + return ResponseEntity.ok(incidentService.findByCreatedBy(user, pageable) + .map(IncidentResponse::fromEntityBasic)); + } + + @PreAuthorize("hasRole('HANDLER')") + @GetMapping("/assigned") + public ResponseEntity> getAssignedIncidents( + @AuthenticationPrincipal CustomUserDetails userDetails, Pageable pageable) { + log.info("GET /api/incidents/assigned - Fetching assigned incidents"); + AppUser user = userDetails.getUser(); + return ResponseEntity.ok(incidentService.findByAssignedTo(user, pageable) + .map(IncidentResponse::fromEntityBasic)); + } + + @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)); + } + + @PreAuthorize("hasAnyRole('RESIDENT', 'HANDLER', 'ADMIN')") + @GetMapping("/{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)) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + AppUser currentUser = userDetails.getUser(); + notificationService.markNotificationAsReadForIncident(currentUser.getId(), id); + + return ResponseEntity.ok(IncidentResponse.fromEntityWithDocuments( + incidentService.getById(id, currentUser))); + } + + @PreAuthorize("hasRole('ADMIN')") + @PatchMapping("/{incidentId}/assign") + 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 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)); + } + + @PreAuthorize("hasAnyRole('ADMIN', 'HANDLER')") + @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)); + } + + @PreAuthorize("hasAnyRole('ADMIN', 'HANDLER')") + @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 ResponseEntity updateStatus(@PathVariable Long incidentId, + @Valid @RequestBody UpdateIncidentStatusRequest request, + @AuthenticationPrincipal CustomUserDetails adminUser) { + log.info("PATCH /api/incidents/{}/status - Updating status to {}", incidentId, request.status()); + Incident updatedIncident = incidentService.updateIncidentStatus(incidentId, request.status(), + adminUser.getUser()); + 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()); + } +} \ No newline at end of file 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..4322db2 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; - @NotBlank(message = "Subject is required") - private String subject; - private String description; - @NotNull(message = "Incident category is required") - private IncidentCategory incidentCategory; -} + private String description; + + @NotNull(message = "Incident category is required") + private IncidentCategory incidentCategory; +} \ No newline at end of file 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..9e469c8 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,35 @@ @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) { - - String userId = userDetails.getUser().getId(); - - return notificationService.getUnreadNotifications(userId).stream().map(NotificationResponse::fromEntity) - .toList(); - } - - @GetMapping("/user/unread-count") - public long getUnreadCount(@AuthenticationPrincipal CustomUserDetails userDetails) { - String userId = userDetails.getUser().getId(); - return notificationService.getUnreadCount(userId); - } - - @PatchMapping(("/{notificationId}/read")) - public void markAsRead(@PathVariable Long notificationId, @AuthenticationPrincipal CustomUserDetails userDetails) { - String userId = userDetails.getUser().getId(); - notificationService.markAsRead(notificationId, userId); - } -} + private final NotificationService notificationService; + + @GetMapping("/user") + public ResponseEntity> getUserNotifications( + @AuthenticationPrincipal CustomUserDetails userDetails) { + String userId = userDetails.getUser().getId(); + 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 ResponseEntity getUnreadCount(@AuthenticationPrincipal CustomUserDetails userDetails) { + String userId = userDetails.getUser().getId(); + log.info("GET /notifications/user/unread-count - Fetching unread count for user: {}", userId); + return ResponseEntity.ok(notificationService.getUnreadCount(userId)); + } + + @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(); + } +} \ No newline at end of file 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..4d4ce13 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; @@ -20,177 +21,214 @@ @Transactional(readOnly = true) public class UserService { - private final AppUserRepository userRepository; - - @Transactional - public AppUser createOrUpdateUser(Map attributes) { - OAuthUserInfo oauthUserInfo = extractOAuthUserInfo(attributes); - - log.info("Processing GitHub login. githubId={}, githubLogin={}", oauthUserInfo.githubId(), - oauthUserInfo.githubLogin()); - - return userRepository.findByGithubId(oauthUserInfo.githubId()) - .map(existingUser -> updateExistingUser(existingUser, oauthUserInfo)) - .orElseGet(() -> createNewPendingUser(oauthUserInfo)); - } - - public AppUser getUserById(String id) { - return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); - } - - public List getAllUsers() { - return userRepository.findAll(); - } - - public List getUsersByRole(UserRole role) { - return userRepository.findByRole(role); - } - - public Page getAllUsersPaginated(Pageable pageable) { - return userRepository.findAll(pageable); - } - - public Page getUsersByRolePaginated(UserRole role, Pageable pageable) { - return userRepository.findByRole(role, pageable); - } - - public Page getUsersWithFilters(String email, String name, UserRole role, Boolean active, - Pageable pageable) { - return userRepository.findAllWithFilters(email, name, role, active, pageable); - } - - public Page searchUsers(String search, Pageable pageable) { - if (search == null || search.trim().isEmpty()) { - return userRepository.findAll(pageable); - } - - return userRepository.searchUsers(search.trim(), pageable); - } - - @Transactional - public AppUser updateUserRole(String userId, UserRole newRole) { - AppUser user = getUserById(userId); - UserRole oldRole = user.getRole(); - - user.setRole(newRole); - AppUser savedUser = userRepository.save(user); - - log.info("Updated role for userId={} from {} to {}", userId, oldRole, newRole); - return savedUser; - } - - @Transactional - public AppUser updateUserActiveStatus(String userId, boolean active) { - AppUser user = getUserById(userId); - - if (!active && user.getRole() == UserRole.ADMIN) { - long activeAdminCount = userRepository.countByRoleAndActiveTrue(UserRole.ADMIN); - if (activeAdminCount <= 1) { - throw new IllegalStateException("Cannot deactivate the last active admin user"); - } - } - - boolean oldStatus = user.isActive(); - user.setActive(active); - AppUser savedUser = userRepository.save(user); - - log.info("Updated active status for userId={} from {} to {}", userId, oldStatus, active); - return savedUser; - } - - @Transactional - public AppUser approvePendingUser(String userId) { - AppUser user = getUserById(userId); - - if (user.getRole() != UserRole.PENDING) { - throw new IllegalStateException("User is not pending approval. Current role: " + user.getRole()); - } - - user.setRole(UserRole.RESIDENT); - user.setActive(true); - - AppUser savedUser = userRepository.save(user); - log.info("Approved pending user: userId={}, githubLogin={}", userId, user.getGithubLogin()); - return savedUser; - } - - @Transactional - public void deleteUser(String userId) { - AppUser user = getUserById(userId); - - if (user.getRole() == UserRole.ADMIN) { - long adminCount = userRepository.countByRole(UserRole.ADMIN); - if (adminCount <= 1) { - throw new IllegalStateException("Cannot delete the last admin user"); - } - } - - userRepository.delete(user); - log.info("Deleted user: userId={}, githubLogin={}", userId, user.getGithubLogin()); - } - - private AppUser updateExistingUser(AppUser existingUser, OAuthUserInfo oauthUserInfo) { - existingUser.setGithubLogin(oauthUserInfo.githubLogin()); - existingUser.setEmail(oauthUserInfo.email()); - existingUser.setName(oauthUserInfo.name()); - existingUser.setAvatarUrl(oauthUserInfo.avatarUrl()); - - AppUser savedUser = userRepository.save(existingUser); - - log.info("Updated existing user. userId={}, githubId={}, role={}, active={}", savedUser.getId(), - savedUser.getGithubId(), savedUser.getRole(), savedUser.isActive()); - - return savedUser; - } - - private AppUser createNewPendingUser(OAuthUserInfo oauthUserInfo) { - AppUser newUser = new AppUser(); - newUser.setGithubId(oauthUserInfo.githubId()); - newUser.setGithubLogin(oauthUserInfo.githubLogin()); - newUser.setEmail(oauthUserInfo.email()); - newUser.setName(oauthUserInfo.name()); - newUser.setAvatarUrl(oauthUserInfo.avatarUrl()); - newUser.setRole(UserRole.PENDING); - newUser.setActive(true); - - AppUser savedUser = userRepository.save(newUser); - - log.info("Created new user. userId={}, githubId={}, role={}", savedUser.getId(), savedUser.getGithubId(), - savedUser.getRole()); - - return savedUser; - } - - private OAuthUserInfo extractOAuthUserInfo(Map attributes) { - String githubId = extractRequiredAttribute(attributes, "id"); - String githubLogin = extractRequiredAttribute(attributes, "login"); - String email = extractOptionalAttribute(attributes, "email"); - String name = resolveDisplayName(attributes, githubLogin); - String avatarUrl = extractOptionalAttribute(attributes, "avatar_url"); - - return new OAuthUserInfo(githubId, githubLogin, email, name, avatarUrl); - } - - private String resolveDisplayName(Map attributes, String githubLogin) { - String name = extractOptionalAttribute(attributes, "name"); - return (name == null || name.isBlank()) ? githubLogin : name; - } - - private String extractRequiredAttribute(Map attributes, String key) { - String value = extractOptionalAttribute(attributes, key); - - if (value == null || value.isBlank()) { - throw new IllegalArgumentException("Missing required OAuth attribute: " + key); - } - - return value; - } - - private String extractOptionalAttribute(Map attributes, String key) { - Object value = attributes.get(key); - return value != null ? String.valueOf(value) : null; - } - - private record OAuthUserInfo(String githubId, String githubLogin, String email, String name, String avatarUrl) { - } -} + private final AppUserRepository userRepository; + private final EntityManager entityManager; + + @Transactional + public AppUser createOrUpdateUser(Map attributes) { + OAuthUserInfo oauthUserInfo = extractOAuthUserInfo(attributes); + + log.info("Processing GitHub login. githubId={}, githubLogin={}", oauthUserInfo.githubId(), + oauthUserInfo.githubLogin()); + + return userRepository.findByGithubId(oauthUserInfo.githubId()) + .map(existingUser -> updateExistingUser(existingUser, oauthUserInfo)) + .orElseGet(() -> createNewPendingUser(oauthUserInfo)); + } + + public AppUser getUserById(String id) { + return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); + } + + public List getAllUsers() { + return userRepository.findAll(); + } + + public List getUsersByRole(UserRole role) { + return userRepository.findByRole(role); + } + + public Page getAllUsersPaginated(Pageable pageable) { + return userRepository.findAll(pageable); + } + + public Page getUsersByRolePaginated(UserRole role, Pageable pageable) { + return userRepository.findByRole(role, pageable); + } + + public Page getUsersWithFilters(String email, String name, UserRole role, Boolean active, + Pageable pageable) { + return userRepository.findAllWithFilters(email, name, role, active, pageable); + } + + public Page searchUsers(String search, Pageable pageable) { + if (search == null || search.trim().isEmpty()) { + return userRepository.findAll(pageable); + } + + return userRepository.searchUsers(search.trim(), pageable); + } + + @Transactional + public AppUser updateUserRole(String userId, UserRole newRole) { + AppUser user = getUserById(userId); + UserRole oldRole = user.getRole(); + + user.setRole(newRole); + AppUser savedUser = userRepository.save(user); + + log.info("Updated role for userId={} from {} to {}", userId, oldRole, newRole); + return savedUser; + } + + @Transactional + public AppUser updateUserActiveStatus(String userId, boolean active) { + AppUser user = getUserById(userId); + + if (!active && user.getRole() == UserRole.ADMIN) { + long activeAdminCount = userRepository.countByRoleAndActiveTrue(UserRole.ADMIN); + if (activeAdminCount <= 1) { + throw new IllegalStateException("Cannot deactivate the last active admin user"); + } + } + + boolean oldStatus = user.isActive(); + user.setActive(active); + AppUser savedUser = userRepository.save(user); + + log.info("Updated active status for userId={} from {} to {}", userId, oldStatus, active); + return savedUser; + } + + @Transactional + public AppUser approvePendingUser(String userId) { + AppUser user = getUserById(userId); + + if (user.getRole() != UserRole.PENDING) { + throw new IllegalStateException("User is not pending approval. Current role: " + user.getRole()); + } + + user.setRole(UserRole.RESIDENT); + user.setActive(true); + + AppUser savedUser = userRepository.save(user); + log.info("Approved pending user: userId={}, githubLogin={}", userId, user.getGithubLogin()); + return savedUser; + } + + @Transactional + public void deleteUser(String userId) { + AppUser user = getUserById(userId); + + if (user.getRole() == UserRole.ADMIN) { + long adminCount = userRepository.countByRole(UserRole.ADMIN); + if (adminCount <= 1) { + throw new IllegalStateException("Cannot delete the last admin user"); + } + } + + 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={}, role={}", userId, user.getGithubLogin(), user.getRole()); + } + + private AppUser updateExistingUser(AppUser existingUser, OAuthUserInfo oauthUserInfo) { + existingUser.setGithubLogin(oauthUserInfo.githubLogin()); + existingUser.setEmail(oauthUserInfo.email()); + existingUser.setName(oauthUserInfo.name()); + existingUser.setAvatarUrl(oauthUserInfo.avatarUrl()); + + AppUser savedUser = userRepository.save(existingUser); + + log.info("Updated existing user. userId={}, githubId={}, role={}, active={}", savedUser.getId(), + savedUser.getGithubId(), savedUser.getRole(), savedUser.isActive()); + + return savedUser; + } + + private AppUser createNewPendingUser(OAuthUserInfo oauthUserInfo) { + AppUser newUser = new AppUser(); + newUser.setGithubId(oauthUserInfo.githubId()); + newUser.setGithubLogin(oauthUserInfo.githubLogin()); + newUser.setEmail(oauthUserInfo.email()); + newUser.setName(oauthUserInfo.name()); + newUser.setAvatarUrl(oauthUserInfo.avatarUrl()); + newUser.setRole(UserRole.PENDING); + newUser.setActive(true); + + AppUser savedUser = userRepository.save(newUser); + + log.info("Created new user. userId={}, githubId={}, role={}", savedUser.getId(), savedUser.getGithubId(), + savedUser.getRole()); + + return savedUser; + } + + private OAuthUserInfo extractOAuthUserInfo(Map attributes) { + String githubId = extractRequiredAttribute(attributes, "id"); + String githubLogin = extractRequiredAttribute(attributes, "login"); + String email = extractOptionalAttribute(attributes, "email"); + String name = resolveDisplayName(attributes, githubLogin); + String avatarUrl = extractOptionalAttribute(attributes, "avatar_url"); + + return new OAuthUserInfo(githubId, githubLogin, email, name, avatarUrl); + } + + private String resolveDisplayName(Map attributes, String githubLogin) { + String name = extractOptionalAttribute(attributes, "name"); + return (name == null || name.isBlank()) ? githubLogin : name; + } + + private String extractRequiredAttribute(Map attributes, String key) { + String value = extractOptionalAttribute(attributes, key); + + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Missing required OAuth attribute: " + key); + } + + return value; + } + + private String extractOptionalAttribute(Map attributes, String key) { + Object value = attributes.get(key); + return value != null ? String.valueOf(value) : null; + } + + private record OAuthUserInfo(String githubId, String githubLogin, String email, String name, String avatarUrl) { + } +} \ No newline at end of file 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 - +