From f323b1bf8c37f79924759bff1b350975d42e3879 Mon Sep 17 00:00:00 2001 From: Andreas Kaiberger Date: Mon, 27 Apr 2026 22:04:51 +0200 Subject: [PATCH 1/5] Add ability to comment on tickets, enhance permissions handling, and improve UI for ticket details --- .../untitled/usercase/AuditAction.java | 7 +- .../usercase/controller/CaseController.java | 70 ++++++++++++++++--- src/main/resources/templates/ticket.html | 47 ++++++++++--- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/example/untitled/usercase/AuditAction.java b/src/main/java/org/example/untitled/usercase/AuditAction.java index 51b38ae..9812584 100644 --- a/src/main/java/org/example/untitled/usercase/AuditAction.java +++ b/src/main/java/org/example/untitled/usercase/AuditAction.java @@ -7,5 +7,10 @@ public enum AuditAction { COMMENT_ADDED, FILE_UPLOADED, USER_ROLE_CHANGED, - CASE_UPDATED + CASE_UPDATED; + + public String getDisplayName() { + String name = this.name().toLowerCase().replace('_', ' '); + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } } diff --git a/src/main/java/org/example/untitled/usercase/controller/CaseController.java b/src/main/java/org/example/untitled/usercase/controller/CaseController.java index e600666..7a58163 100644 --- a/src/main/java/org/example/untitled/usercase/controller/CaseController.java +++ b/src/main/java/org/example/untitled/usercase/controller/CaseController.java @@ -2,12 +2,12 @@ import jakarta.validation.Valid; import org.example.untitled.user.Role; +import org.example.untitled.usercase.AuditLog; import org.example.untitled.usercase.CaseStatus; import org.example.untitled.usercase.dto.CaseEntityDto; import org.example.untitled.usercase.dto.CommentDto; import org.example.untitled.usercase.dto.CreateCaseRequest; import org.example.untitled.usercase.dto.CreateCommentRequest; -import org.example.untitled.usercase.AuditLog; import org.example.untitled.usercase.service.AuditLogService; import org.example.untitled.usercase.service.CaseService; import org.example.untitled.usercase.service.CommentService; @@ -37,7 +37,6 @@ public class CaseController { private final AuditLogService auditLogService; private static final Logger log = LoggerFactory.getLogger(CaseController.class); - public CaseController(CaseService caseService, CommentService commentService, AuditLogService auditLogService) { this.caseService = caseService; this.commentService = commentService; @@ -74,10 +73,19 @@ public String showTicketDetails( if (!isHandler && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); + boolean isTicketOpen = ticket.status() != CaseStatus.CLOSED + && ticket.status() != CaseStatus.SOLVED; + boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); + boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); + boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); + List comments = commentService.getCommentsByTicketId(id); List auditLogs = auditLogService.getLogsForCase(id); + model.addAttribute("ticket", ticket); model.addAttribute("comments", comments); + model.addAttribute("comment", new CreateCommentRequest()); + model.addAttribute("canClose", canClose); model.addAttribute("auditLogs", auditLogs); return "ticket"; } @@ -120,11 +128,19 @@ public ResponseEntity assignToSelf( public String closeTicket( Model model, @PathVariable long id, - @AuthenticationPrincipal UserDetails userDetails - ) { + @AuthenticationPrincipal UserDetails userDetails) { CaseEntityDto ticket = caseService.getTicketByID(id); - if (caseService.isNotOwner(ticket, userDetails.getUsername())) + + boolean isHandler = userDetails.getAuthorities().stream() + .map(a -> Role.fromAuthority(a.getAuthority())) + .flatMap(Optional::stream) + .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); + + boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); + + if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); + model.addAttribute("ticket", caseService.getTicketByID(id)); model.addAttribute("comment", new CreateCommentRequest()); return "close_ticket"; @@ -136,11 +152,19 @@ public String processCloseTicket( @ModelAttribute("comment") @Valid CreateCommentRequest comment, BindingResult bindingResult, @AuthenticationPrincipal UserDetails userDetails, - Model model - ) { + Model model) { CaseEntityDto ticket = caseService.getTicketByID(id); - if (caseService.isNotOwner(ticket, userDetails.getUsername())) + + boolean isHandler = userDetails.getAuthorities().stream() + .map(a -> Role.fromAuthority(a.getAuthority())) + .flatMap(Optional::stream) + .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); + + boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); + + if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); + if (bindingResult.hasErrors()) { model.addAttribute("ticket", ticket); return "close_ticket"; @@ -154,6 +178,9 @@ public String processCloseTicket( return "close_ticket"; } + if (isHandler) { + return "redirect:/handler"; + } return "redirect:/user"; } @@ -218,4 +245,31 @@ public String updateStatusForm( } return "redirect:/handler"; } + + @PostMapping("/{id}/comments") + @PreAuthorize("isAuthenticated()") + public String addComment( + @PathVariable long id, + @Valid @ModelAttribute("comment") CreateCommentRequest comment, + BindingResult bindingResult, + @AuthenticationPrincipal UserDetails userDetails, + Model model, + RedirectAttributes redirectAttributes) { + if (bindingResult.hasErrors()) { + CaseEntityDto ticket = caseService.getTicketByID(id); + model.addAttribute("ticket", ticket); + model.addAttribute("comments", commentService.getCommentsByTicketId(id)); + model.addAttribute("auditLogs", auditLogService.getLogsForCase(id)); + return "ticket"; + } + try { + comment.setCaseId(id); + CaseEntityDto ticket = caseService.getTicketByID(id); + commentService.createComment(comment, ticket); + redirectAttributes.addFlashAttribute("success", "Comment added successfully"); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("error", "Could not add comment"); + } + return "redirect:/tickets/" + id; + } } diff --git a/src/main/resources/templates/ticket.html b/src/main/resources/templates/ticket.html index 8650650..5f83be5 100644 --- a/src/main/resources/templates/ticket.html +++ b/src/main/resources/templates/ticket.html @@ -10,6 +10,10 @@

Ticket Details

+ +
+
+
@@ -26,27 +30,50 @@

Ticket Details

-
+ +

Comments

-
-

-

Author:

+
+
+ + +
+
+

+
-
+ +
+

Add Comment

+
+
+ + +
+
+ +
+
+
+ + +

Activity Log

-
+
- - + +
- \ No newline at end of file + From 0729516e39bec12a230be55ac7efae88aea8d211 Mon Sep 17 00:00:00 2001 From: Andreas Kaiberger Date: Mon, 27 Apr 2026 22:12:32 +0200 Subject: [PATCH 2/5] Integrate user details into activity logs, enhance ticket UI with dynamic elements, and refine styling for comment and activity sections --- .../usercase/controller/CaseController.java | 28 +++++++++++++++-- src/main/resources/static/style.css | 31 +++++++++++++++++++ src/main/resources/templates/ticket.html | 18 ++++++----- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/untitled/usercase/controller/CaseController.java b/src/main/java/org/example/untitled/usercase/controller/CaseController.java index 7a58163..f5a04c2 100644 --- a/src/main/java/org/example/untitled/usercase/controller/CaseController.java +++ b/src/main/java/org/example/untitled/usercase/controller/CaseController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import org.example.untitled.user.Role; +import org.example.untitled.user.service.UserService; import org.example.untitled.usercase.AuditLog; import org.example.untitled.usercase.CaseStatus; import org.example.untitled.usercase.dto.CaseEntityDto; @@ -26,7 +27,9 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; @Controller @RequestMapping("/tickets") @@ -35,12 +38,15 @@ public class CaseController { private final CaseService caseService; private final CommentService commentService; private final AuditLogService auditLogService; + private final UserService userService; private static final Logger log = LoggerFactory.getLogger(CaseController.class); - public CaseController(CaseService caseService, CommentService commentService, AuditLogService auditLogService) { + public CaseController(CaseService caseService, CommentService commentService, + AuditLogService auditLogService, UserService userService) { this.caseService = caseService; this.commentService = commentService; this.auditLogService = auditLogService; + this.userService = userService; } @PostMapping @@ -82,11 +88,20 @@ public String showTicketDetails( List comments = commentService.getCommentsByTicketId(id); List auditLogs = auditLogService.getLogsForCase(id); + Map auditUserMap = auditLogs.stream() + .filter(a -> a.getUserId() != null) + .map(a -> userService.findById(a.getUserId())) + .collect(Collectors.toMap( + u -> u.getId(), + u -> u.getUsername(), + (a, b) -> a)); + model.addAttribute("ticket", ticket); model.addAttribute("comments", comments); model.addAttribute("comment", new CreateCommentRequest()); model.addAttribute("canClose", canClose); model.addAttribute("auditLogs", auditLogs); + model.addAttribute("auditUserMap", auditUserMap); return "ticket"; } @@ -257,9 +272,18 @@ public String addComment( RedirectAttributes redirectAttributes) { if (bindingResult.hasErrors()) { CaseEntityDto ticket = caseService.getTicketByID(id); + List auditLogs = auditLogService.getLogsForCase(id); + Map auditUserMap = auditLogs.stream() + .filter(a -> a.getUserId() != null) + .map(a -> userService.findById(a.getUserId())) + .collect(Collectors.toMap( + u -> u.getId(), + u -> u.getUsername(), + (a, b) -> a)); model.addAttribute("ticket", ticket); model.addAttribute("comments", commentService.getCommentsByTicketId(id)); - model.addAttribute("auditLogs", auditLogService.getLogsForCase(id)); + model.addAttribute("auditLogs", auditLogs); + model.addAttribute("auditUserMap", auditUserMap); return "ticket"; } try { diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index 7779dcb..b32c1c8 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -430,3 +430,34 @@ main { margin: 40px auto; padding: 0 20px; } + +/* Comment section */ + +.comment-box { + background: #1e293b; + border: 1px solid #334155; + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 12px; +} + +.comment-author { + font-size: 13px; + color: #64748b; + margin-top: 8px; +} + +.comment-text { + font-size: 14px; + line-height: 1.6; + color: #cbd5e1; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: #f1f5f9; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #1e3a5f; +} diff --git a/src/main/resources/templates/ticket.html b/src/main/resources/templates/ticket.html index 5f83be5..0692e36 100644 --- a/src/main/resources/templates/ticket.html +++ b/src/main/resources/templates/ticket.html @@ -32,7 +32,7 @@

Ticket Details

-

Comments

+

Comments

@@ -46,8 +46,8 @@

Comments

-
-

Add Comment

+
+

Add Comment

@@ -61,12 +61,14 @@

Add Comment

-

Activity Log

+

Activity Log

-
- - +
+

+

+

From 3ed16c749aafba0c197d749dc5c6ecf9b8266b17 Mon Sep 17 00:00:00 2001 From: Andreas Kaiberger Date: Mon, 27 Apr 2026 22:33:23 +0200 Subject: [PATCH 3/5] Integrate user details into activity logs, enhance ticket UI with dynamic elements, and refine styling for comment and activity sections --- .../untitled/user/service/UserService.java | 8 ++ .../usercase/controller/CaseController.java | 98 ++++++++++++------- .../usercase/service/CaseService.java | 19 ++-- .../usercase/service/CommentService.java | 15 ++- src/main/resources/templates/ticket.html | 13 ++- 5 files changed, 100 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/example/untitled/user/service/UserService.java b/src/main/java/org/example/untitled/user/service/UserService.java index b90dc72..ef7b319 100644 --- a/src/main/java/org/example/untitled/user/service/UserService.java +++ b/src/main/java/org/example/untitled/user/service/UserService.java @@ -17,7 +17,10 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service public class UserService { @@ -86,4 +89,9 @@ public User findById(Long id) { return userRep.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); } + + public Map findAllByIds(Collection ids) { + return userRep.findAllById(ids).stream() + .collect(Collectors.toMap(User::getId, User::getUsername)); + } } diff --git a/src/main/java/org/example/untitled/usercase/controller/CaseController.java b/src/main/java/org/example/untitled/usercase/controller/CaseController.java index f5a04c2..2da19e8 100644 --- a/src/main/java/org/example/untitled/usercase/controller/CaseController.java +++ b/src/main/java/org/example/untitled/usercase/controller/CaseController.java @@ -14,6 +14,7 @@ import org.example.untitled.usercase.service.CommentService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -29,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Controller @@ -71,10 +73,7 @@ public String showTicketDetails( CaseEntityDto ticket = caseService.getTicketByID(id); - boolean isHandler = userDetails.getAuthorities().stream() - .map(a -> Role.fromAuthority(a.getAuthority())) - .flatMap(Optional::stream) - .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); + boolean isHandler = isHandlerOrAbove(userDetails); if (!isHandler && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); @@ -84,22 +83,17 @@ public String showTicketDetails( boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); + boolean canComment = isOwner || isAssigned || isHandler; List comments = commentService.getCommentsByTicketId(id); List auditLogs = auditLogService.getLogsForCase(id); - - Map auditUserMap = auditLogs.stream() - .filter(a -> a.getUserId() != null) - .map(a -> userService.findById(a.getUserId())) - .collect(Collectors.toMap( - u -> u.getId(), - u -> u.getUsername(), - (a, b) -> a)); + Map auditUserMap = buildAuditUserMap(auditLogs); model.addAttribute("ticket", ticket); model.addAttribute("comments", comments); model.addAttribute("comment", new CreateCommentRequest()); model.addAttribute("canClose", canClose); + model.addAttribute("canComment", canComment); model.addAttribute("auditLogs", auditLogs); model.addAttribute("auditUserMap", auditUserMap); return "ticket"; @@ -146,17 +140,13 @@ public String closeTicket( @AuthenticationPrincipal UserDetails userDetails) { CaseEntityDto ticket = caseService.getTicketByID(id); - boolean isHandler = userDetails.getAuthorities().stream() - .map(a -> Role.fromAuthority(a.getAuthority())) - .flatMap(Optional::stream) - .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); - + boolean isHandler = isHandlerOrAbove(userDetails); boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); - model.addAttribute("ticket", caseService.getTicketByID(id)); + model.addAttribute("ticket", ticket); model.addAttribute("comment", new CreateCommentRequest()); return "close_ticket"; } @@ -170,11 +160,7 @@ public String processCloseTicket( Model model) { CaseEntityDto ticket = caseService.getTicketByID(id); - boolean isHandler = userDetails.getAuthorities().stream() - .map(a -> Role.fromAuthority(a.getAuthority())) - .flatMap(Optional::stream) - .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); - + boolean isHandler = isHandlerOrAbove(userDetails); boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) @@ -186,7 +172,7 @@ public String processCloseTicket( } comment.setCaseId(id); try { - caseService.closeTicket(ticket, comment); + caseService.closeTicket(ticket, comment, userDetails.getUsername()); } catch (IllegalArgumentException e) { bindingResult.rejectValue("text", "error.createCommentRequest", e.getMessage()); model.addAttribute("ticket", ticket); @@ -270,30 +256,72 @@ public String addComment( @AuthenticationPrincipal UserDetails userDetails, Model model, RedirectAttributes redirectAttributes) { + + CaseEntityDto ticket = caseService.getTicketByID(id); + + boolean isHandler = isHandlerOrAbove(userDetails); + boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); + boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); + boolean canComment = isOwner || isAssigned || isHandler; + + if (!canComment) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not allowed to comment on this ticket"); + } + + boolean isTicketOpen = ticket.status() != CaseStatus.CLOSED + && ticket.status() != CaseStatus.SOLVED; + boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); + if (bindingResult.hasErrors()) { - CaseEntityDto ticket = caseService.getTicketByID(id); List auditLogs = auditLogService.getLogsForCase(id); - Map auditUserMap = auditLogs.stream() - .filter(a -> a.getUserId() != null) - .map(a -> userService.findById(a.getUserId())) - .collect(Collectors.toMap( - u -> u.getId(), - u -> u.getUsername(), - (a, b) -> a)); + Map auditUserMap = buildAuditUserMap(auditLogs); model.addAttribute("ticket", ticket); model.addAttribute("comments", commentService.getCommentsByTicketId(id)); + model.addAttribute("canClose", canClose); + model.addAttribute("canComment", canComment); model.addAttribute("auditLogs", auditLogs); model.addAttribute("auditUserMap", auditUserMap); return "ticket"; } + try { comment.setCaseId(id); - CaseEntityDto ticket = caseService.getTicketByID(id); - commentService.createComment(comment, ticket); + commentService.createComment(comment, ticket, userDetails.getUsername()); redirectAttributes.addFlashAttribute("success", "Comment added successfully"); + } catch (IllegalArgumentException e) { + log.warn("Comment creation failed for ticket {} by user {}: {}", id, userDetails.getUsername(), e.getMessage()); + redirectAttributes.addFlashAttribute("error", e.getMessage()); + } catch (DataAccessException e) { + log.error("Data access error adding comment to ticket {} by user {}.", id, userDetails.getUsername(), e); + redirectAttributes.addFlashAttribute("error", "Could not save comment, please try again"); + } catch (ResponseStatusException e) { + log.warn("Comment creation rejected for ticket {} by user {}: {}", id, userDetails.getUsername(), e.getReason(), e); + redirectAttributes.addFlashAttribute("error", + e.getReason() != null ? e.getReason() : "Could not add comment"); } catch (Exception e) { - redirectAttributes.addFlashAttribute("error", "Could not add comment"); + log.error("Unexpected error adding comment to ticket {} by user {}.", id, userDetails.getUsername(), e); + redirectAttributes.addFlashAttribute("error", "An unexpected error occurred"); } return "redirect:/tickets/" + id; } + + // --- Private helpers --- + + private boolean isHandlerOrAbove(UserDetails userDetails) { + return userDetails.getAuthorities().stream() + .map(a -> Role.fromAuthority(a.getAuthority())) + .flatMap(Optional::stream) + .anyMatch(r -> r == Role.HANDLER || r == Role.SUPERVISOR || r == Role.ADMIN); + } + + private Map buildAuditUserMap(List auditLogs) { + Set userIds = auditLogs.stream() + .map(AuditLog::getUserId) + .filter(uid -> uid != null) + .collect(Collectors.toSet()); + if (userIds.isEmpty()) { + return Map.of(); + } + return userService.findAllByIds(userIds); + } } diff --git a/src/main/java/org/example/untitled/usercase/service/CaseService.java b/src/main/java/org/example/untitled/usercase/service/CaseService.java index 46b5dbc..4918792 100644 --- a/src/main/java/org/example/untitled/usercase/service/CaseService.java +++ b/src/main/java/org/example/untitled/usercase/service/CaseService.java @@ -61,8 +61,8 @@ public CaseEntityDto createTicket(CreateCaseRequest request, String username) { caseEntity.setOwner(owner); caseEntity.setStatus(CaseStatus.OPEN); caseEntity = caseRepository.save(caseEntity); - if (request.getFileNames() != null){ - for(String fName : request.getFileNames()){ + if (request.getFileNames() != null) { + for (String fName : request.getFileNames()) { if (fName == null || fName.isBlank()) continue; caseEntity.getFiles().addAll(s3Service.createFile(caseEntity, fName)); } @@ -96,11 +96,11 @@ public CaseEntityDto updateTicket(Long id, CreateCaseRequest request, String use } caseEntity.setTitle(request.getTitle()); caseEntity.setDescription(request.getDescription()); - if (request.getFileNames() != null){ + if (request.getFileNames() != null) { Set existing = caseEntity.getFiles().stream() .map(UploadedFile::getS3Key) .collect(Collectors.toSet()); - for(String fName : request.getFileNames()){ + for (String fName : request.getFileNames()) { if (fName == null || fName.isBlank() || existing.contains(fName)) continue; caseEntity.getFiles().addAll(s3Service.createFile(caseEntity, fName)); } @@ -114,13 +114,13 @@ public CaseEntityDto updateTicket(Long id, CreateCaseRequest request, String use } @Transactional - public void closeTicket(CaseEntityDto ticket, CreateCommentRequest comment) { + public void closeTicket(CaseEntityDto ticket, CreateCommentRequest comment, String username) { if (comment == null) - throw new IllegalArgumentException("Comment Cant be null"); + throw new IllegalArgumentException("Comment can't be null"); if (ticket == null) - throw new IllegalArgumentException("Ticket Cant be null"); - updateStatus(ticket.id(), CaseStatus.CLOSED, ticket.ownerUsername()); - commentService.createComment(comment, ticket); + throw new IllegalArgumentException("Ticket can't be null"); + updateStatus(ticket.id(), CaseStatus.CLOSED, username); + commentService.createComment(comment, ticket, username); } public List getAllTickets() { @@ -170,6 +170,7 @@ public CaseEntityDto getTicketByID(long id) { () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found: " + id)); return CaseMapper.toDto(caseEntity); } + public User findOwnerById(long id) { return caseRepository.findOwnerById(id); } diff --git a/src/main/java/org/example/untitled/usercase/service/CommentService.java b/src/main/java/org/example/untitled/usercase/service/CommentService.java index 5a14aef..692f8e1 100644 --- a/src/main/java/org/example/untitled/usercase/service/CommentService.java +++ b/src/main/java/org/example/untitled/usercase/service/CommentService.java @@ -1,5 +1,6 @@ package org.example.untitled.usercase.service; +import org.example.untitled.user.repository.UserRepository; import org.example.untitled.usercase.dto.CommentDto; import org.example.untitled.usercase.AuditAction; import org.example.untitled.usercase.dto.CaseEntityDto; @@ -18,27 +19,31 @@ public class CommentService { private final CommentRepository commentRepository; private final CaseRepository caseRepository; private final AuditLogService auditLogService; + private final UserRepository userRepository; - - public CommentService(CommentRepository commentRepository, CaseRepository caseRepository, AuditLogService auditLogService) { + public CommentService(CommentRepository commentRepository, CaseRepository caseRepository, + AuditLogService auditLogService, UserRepository userRepository) { this.commentRepository = commentRepository; this.caseRepository = caseRepository; this.auditLogService = auditLogService; + this.userRepository = userRepository; } @Transactional - public void createComment(CreateCommentRequest comment, CaseEntityDto ticket) { + public void createComment(CreateCommentRequest comment, CaseEntityDto ticket, String username) { if (comment == null) throw new IllegalArgumentException("CreateCommentRequest can't be null"); if (ticket == null) throw new IllegalArgumentException("CaseEntityDTO can't be null"); var caseEntity = caseRepository.findById(ticket.id()) .orElseThrow(() -> new IllegalArgumentException("Ticket not found: " + ticket.id())); + var author = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + username)); var entity = CommentMapper.toEntity(comment); - entity.setAuthor(caseEntity.getOwner()); + entity.setAuthor(author); entity.setCaseEntity(caseEntity); commentRepository.save(entity); - auditLogService.log(AuditAction.COMMENT_ADDED, entity.getAuthor().getId(), caseEntity.getId()); // lägg till + auditLogService.log(AuditAction.COMMENT_ADDED, author.getId(), caseEntity.getId()); } public List getCommentsByTicketId(Long id) { diff --git a/src/main/resources/templates/ticket.html b/src/main/resources/templates/ticket.html index 0692e36..b7ce774 100644 --- a/src/main/resources/templates/ticket.html +++ b/src/main/resources/templates/ticket.html @@ -46,12 +46,17 @@

Comments

-
+

Add Comment

- +
- + +
@@ -66,7 +71,7 @@

Activity Log

+ th:text="'By: ' + ${auditUserMap[entry.userId] ?: 'Unknown user'}">

From ee97db35a5a018300862096ee70bf45eee91befc Mon Sep 17 00:00:00 2001 From: Andreas Kaiberger Date: Mon, 27 Apr 2026 22:47:00 +0200 Subject: [PATCH 4/5] Update AuditAction to use Locale.ROOT in getDisplayName for consistent case conversion. --- src/main/java/org/example/untitled/usercase/AuditAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/untitled/usercase/AuditAction.java b/src/main/java/org/example/untitled/usercase/AuditAction.java index 9812584..79ffe53 100644 --- a/src/main/java/org/example/untitled/usercase/AuditAction.java +++ b/src/main/java/org/example/untitled/usercase/AuditAction.java @@ -1,5 +1,7 @@ package org.example.untitled.usercase; +import java.util.Locale; + public enum AuditAction { CASE_CREATED, CASE_ASSIGNED, @@ -10,7 +12,7 @@ public enum AuditAction { CASE_UPDATED; public String getDisplayName() { - String name = this.name().toLowerCase().replace('_', ' '); + String name = this.name().toLowerCase(Locale.ROOT).replace('_', ' '); return Character.toUpperCase(name.charAt(0)) + name.substring(1); } } From 6a1de488920494b26ffad4bde8711583e8f8416f Mon Sep 17 00:00:00 2001 From: Andreas Kaiberger Date: Mon, 27 Apr 2026 23:51:23 +0200 Subject: [PATCH 5/5] Refactor permissions handling into `TicketPermissions` record for improved reusability and streamlined controller logic --- .../usercase/controller/CaseController.java | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/example/untitled/usercase/controller/CaseController.java b/src/main/java/org/example/untitled/usercase/controller/CaseController.java index 2da19e8..9d9737d 100644 --- a/src/main/java/org/example/untitled/usercase/controller/CaseController.java +++ b/src/main/java/org/example/untitled/usercase/controller/CaseController.java @@ -51,6 +51,29 @@ public CaseController(CaseService caseService, CommentService commentService, this.userService = userService; } + /** + * Encapsulates permission checks for a given ticket and user so the + * same logic is not duplicated across multiple handler methods. + */ + private record TicketPermissions( + boolean isHandler, + boolean isOwner, + boolean isAssigned, + boolean canClose, + boolean canComment + ) { + static TicketPermissions of(CaseEntityDto ticket, UserDetails userDetails, + boolean isHandler) { + boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); + boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); + boolean isTicketOpen = ticket.status() != CaseStatus.CLOSED + && ticket.status() != CaseStatus.SOLVED; + boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); + boolean canComment = isOwner || isAssigned || isHandler; + return new TicketPermissions(isHandler, isOwner, isAssigned, canClose, canComment); + } + } + @PostMapping public ResponseEntity createTicket( @Valid @RequestBody CreateCaseRequest request, @@ -72,19 +95,11 @@ public String showTicketDetails( Model model) { CaseEntityDto ticket = caseService.getTicketByID(id); + TicketPermissions perms = TicketPermissions.of(ticket, userDetails, isHandlerOrAbove(userDetails)); - boolean isHandler = isHandlerOrAbove(userDetails); - - if (!isHandler && caseService.isNotOwner(ticket, userDetails.getUsername())) + if (!perms.isHandler() && caseService.isNotOwner(ticket, userDetails.getUsername())) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); - boolean isTicketOpen = ticket.status() != CaseStatus.CLOSED - && ticket.status() != CaseStatus.SOLVED; - boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); - boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); - boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); - boolean canComment = isOwner || isAssigned || isHandler; - List comments = commentService.getCommentsByTicketId(id); List auditLogs = auditLogService.getLogsForCase(id); Map auditUserMap = buildAuditUserMap(auditLogs); @@ -92,8 +107,8 @@ public String showTicketDetails( model.addAttribute("ticket", ticket); model.addAttribute("comments", comments); model.addAttribute("comment", new CreateCommentRequest()); - model.addAttribute("canClose", canClose); - model.addAttribute("canComment", canComment); + model.addAttribute("canClose", perms.canClose()); + model.addAttribute("canComment", perms.canComment()); model.addAttribute("auditLogs", auditLogs); model.addAttribute("auditUserMap", auditUserMap); return "ticket"; @@ -139,12 +154,10 @@ public String closeTicket( @PathVariable long id, @AuthenticationPrincipal UserDetails userDetails) { CaseEntityDto ticket = caseService.getTicketByID(id); + TicketPermissions perms = TicketPermissions.of(ticket, userDetails, isHandlerOrAbove(userDetails)); - boolean isHandler = isHandlerOrAbove(userDetails); - boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); - - if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) - throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); + if (!perms.isHandler() && !perms.isAssigned() && caseService.isNotOwner(ticket, userDetails.getUsername())) + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not allowed to close this ticket"); model.addAttribute("ticket", ticket); model.addAttribute("comment", new CreateCommentRequest()); @@ -159,12 +172,10 @@ public String processCloseTicket( @AuthenticationPrincipal UserDetails userDetails, Model model) { CaseEntityDto ticket = caseService.getTicketByID(id); + TicketPermissions perms = TicketPermissions.of(ticket, userDetails, isHandlerOrAbove(userDetails)); - boolean isHandler = isHandlerOrAbove(userDetails); - boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); - - if (!isHandler && !isAssigned && caseService.isNotOwner(ticket, userDetails.getUsername())) - throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not own this ticket"); + if (!perms.isHandler() && !perms.isAssigned() && caseService.isNotOwner(ticket, userDetails.getUsername())) + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not allowed to close this ticket"); if (bindingResult.hasErrors()) { model.addAttribute("ticket", ticket); @@ -179,10 +190,7 @@ public String processCloseTicket( return "close_ticket"; } - if (isHandler) { - return "redirect:/handler"; - } - return "redirect:/user"; + return perms.isHandler() ? "redirect:/handler" : "redirect:/user"; } @PostMapping("/{id}/assign") @@ -258,27 +266,19 @@ public String addComment( RedirectAttributes redirectAttributes) { CaseEntityDto ticket = caseService.getTicketByID(id); + TicketPermissions perms = TicketPermissions.of(ticket, userDetails, isHandlerOrAbove(userDetails)); - boolean isHandler = isHandlerOrAbove(userDetails); - boolean isOwner = ticket.ownerUsername().equals(userDetails.getUsername()); - boolean isAssigned = userDetails.getUsername().equals(ticket.assignedToUsername()); - boolean canComment = isOwner || isAssigned || isHandler; - - if (!canComment) { + if (!perms.canComment()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not allowed to comment on this ticket"); } - boolean isTicketOpen = ticket.status() != CaseStatus.CLOSED - && ticket.status() != CaseStatus.SOLVED; - boolean canClose = isTicketOpen && (isOwner || isAssigned || isHandler); - if (bindingResult.hasErrors()) { List auditLogs = auditLogService.getLogsForCase(id); Map auditUserMap = buildAuditUserMap(auditLogs); model.addAttribute("ticket", ticket); model.addAttribute("comments", commentService.getCommentsByTicketId(id)); - model.addAttribute("canClose", canClose); - model.addAttribute("canComment", canComment); + model.addAttribute("canClose", perms.canClose()); + model.addAttribute("canComment", perms.canComment()); model.addAttribute("auditLogs", auditLogs); model.addAttribute("auditUserMap", auditUserMap); return "ticket";