diff --git a/src/main/java/org/example/alfs/controllers/TicketController.java b/src/main/java/org/example/alfs/controllers/TicketController.java index 23ade1d..5e47799 100644 --- a/src/main/java/org/example/alfs/controllers/TicketController.java +++ b/src/main/java/org/example/alfs/controllers/TicketController.java @@ -1,9 +1,11 @@ package org.example.alfs.controllers; import jakarta.validation.Valid; +import org.example.alfs.dto.ticket.TicketAssignDTO; import org.example.alfs.dto.ticket.TicketCreateDTO; +import org.example.alfs.dto.ticket.TicketStatusUpdateDTO; import org.example.alfs.dto.ticket.TicketViewDTO; -import org.example.alfs.entities.Ticket; +import org.springframework.security.access.prepost.PreAuthorize; import org.example.alfs.services.TicketService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -16,7 +18,7 @@ @RequestMapping public class TicketController { - TicketService ticketService; + private final TicketService ticketService; public TicketController(TicketService ticketService) { @@ -25,7 +27,7 @@ public TicketController(TicketService ticketService) { } //create ticket - + @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @GetMapping("/create") public String createNewTicketForm(Model model) { model.addAttribute("ticket", new TicketCreateDTO()); @@ -33,6 +35,7 @@ public String createNewTicketForm(Model model) { } //TODO REDIRECT, WHERE?? + @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @PostMapping("/create") public String createNewTicket(@ModelAttribute("ticket") @Valid TicketCreateDTO ticketCreateDTO, BindingResult bindingResult) { if (bindingResult.hasErrors()) { @@ -46,7 +49,7 @@ public String createNewTicket(@ModelAttribute("ticket") @Valid TicketCreateDTO t } //view ticket by token - + //TODO, FIX SO ANONYMOUS USERS CAN USE @GetMapping("/view/token/{token}") public String viewTicketByToken(@PathVariable String token, Model model) { @@ -57,6 +60,7 @@ public String viewTicketByToken(@PathVariable String token, Model model) { } //view ticket by id + @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')") @GetMapping("/view/id/{id}") public String viewTicketById(@PathVariable Long id, Model model) { @@ -66,18 +70,42 @@ public String viewTicketById(@PathVariable Long id, Model model) { return "view"; } + //assign ticket - // TODO implement DTO + service + @PreAuthorize("hasRole('ADMIN')") @PostMapping("/{id}/assign") - public String assignTicket(@PathVariable Long id) { - return "redirect:/tickets/" + id; + public String assignTicket(@PathVariable Long id, @Valid @ModelAttribute TicketAssignDTO dto) { + + ticketService.assignInvestigator(id, dto.getInvestigatorId()); + + return "redirect:/view/id/" + id; } //update status - // TODO implement DTO + service @PostMapping("/{id}/status") - public String updateStatus(@PathVariable Long id) { - return "redirect:/tickets/" + id; + @PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR')") + public String updateStatus(@PathVariable Long id, @Valid @ModelAttribute TicketStatusUpdateDTO dto) { + + ticketService.updateTicketStatus(id, dto.getStatus()); + + return "redirect:/view/id/" + id; + } + + + @PreAuthorize("hasRole('REPORTER')") + @GetMapping("/my") + public String myTickets(Model model) { + + model.addAttribute("tickets", ticketService.getMyTickets()); + return "my-tickets"; + } + + @PreAuthorize("hasRole('INVESTIGATOR')") + @GetMapping("/assigned") + public String myAssignedTickets(Model model) { + + model.addAttribute("tickets", ticketService.getMyAssignedTickets()); + return "assigned-tickets"; } //create comment diff --git a/src/main/java/org/example/alfs/services/TicketService.java b/src/main/java/org/example/alfs/services/TicketService.java index 029736e..42c87ab 100644 --- a/src/main/java/org/example/alfs/services/TicketService.java +++ b/src/main/java/org/example/alfs/services/TicketService.java @@ -9,6 +9,7 @@ import org.example.alfs.mapper.TicketMapper; import org.example.alfs.repositories.TicketRepository; import org.example.alfs.repositories.UserRepository; +import org.example.alfs.security.SecurityUtils; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,21 +24,30 @@ public class TicketService { private final TicketRepository ticketRepository; private final TicketMapper ticketMapper; + private final SecurityUtils securityUtils; private final UserRepository userRepository; - public TicketService(TicketRepository ticketRepository, TicketMapper ticketMapper, UserRepository userRepository) { + public TicketService(TicketRepository ticketRepository, + TicketMapper ticketMapper, + SecurityUtils securityUtils, + UserRepository userRepository) { this.ticketRepository = ticketRepository; this.ticketMapper = ticketMapper; + this.securityUtils = securityUtils; this.userRepository = userRepository; } //createNewTicket public TicketViewDTO createNewTicket(TicketCreateDTO ticketCreateDTO) { + Ticket ticket = new Ticket(); ticket.setTitle(ticketCreateDTO.getTitle()); ticket.setDescription(ticketCreateDTO.getDescription()); + User user = requireCurrentUser(); + ticket.setReporter(user); + Ticket savedTicket = ticketRepository.save(ticket); return ticketMapper.entityToViewDTO(savedTicket); @@ -45,172 +55,266 @@ public TicketViewDTO createNewTicket(TicketCreateDTO ticketCreateDTO) { // View by token public TicketViewDTO getTicketByToken(String token) { - Ticket ticket = ticketRepository.findByReporterToken(token). - orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + Ticket ticket = ticketRepository.findByReporterToken(token) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); return ticketMapper.entityToViewDTO(ticket); } //findById public TicketViewDTO getTicketById(Long id) { - Ticket ticket = ticketRepository.findById(id). - orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + Ticket ticket = ticketRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + checkAccess(ticket); return ticketMapper.entityToViewDTO(ticket); } - //findByReporterId - public List getTicketsByReporterId(Long reporterId) { - return ticketRepository.findByReporterId(reporterId) + // Get all tickets for a reporter + public List getMyTickets() { + User user = requireCurrentUser(); + + return ticketRepository.findByReporterId(user.getId()) .stream() .map(ticketMapper::entityToViewDTO) .toList(); } - //findByInvestigatorId - public List getTicketsByInvestigatorId(Long investigatorId) { - return ticketRepository.findByInvestigatorId(investigatorId) + // Get all tickets assigned to me + public List getMyAssignedTickets() { + User user = requireCurrentUser(); + + return ticketRepository.findByInvestigatorId(user.getId()) .stream() .map(ticketMapper::entityToViewDTO) .toList(); } - //findByStatus + // ----------------- filters ----------------- + public List getTicketsByStatus(TicketStatus status) { + User user = requireCurrentUser(); + requireAdmin(user); + return ticketRepository.findByStatus(status) .stream() .map(ticketMapper::entityToViewDTO) .toList(); } - //findByStatusAndInvestigatorId - public List getTicketsByStatusAndInvestigator( - TicketStatus status, - Long investigatorId) { - return ticketRepository - .findByStatusAndInvestigatorId(status, investigatorId) - .stream() - .map(ticketMapper::entityToViewDTO) - .toList(); + public List getTicketsByStatusAndInvestigator(TicketStatus status, Long investigatorId) { + User user = requireCurrentUser(); + + if (user.getRole() == Role.ADMIN) { + return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId) + .stream() + .map(ticketMapper::entityToViewDTO) + .toList(); + } + + if (user.getRole() == Role.INVESTIGATOR && + user.getId().equals(investigatorId)) { + + return ticketRepository.findByStatusAndInvestigatorId(status, investigatorId) + .stream() + .map(ticketMapper::entityToViewDTO) + .toList(); + } + + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); + } + + // ----------------- helpers ----------------- + + private void checkAccess(Ticket ticket) { + User user = requireCurrentUser(); + + if (user.getRole() == Role.ADMIN) return; + + if (user.getRole() == Role.INVESTIGATOR) { + if (ticket.getInvestigator() != null && + ticket.getInvestigator().getId().equals(user.getId())) { + return; + } + } + + if (user.getRole() == Role.REPORTER) { + if (ticket.getReporter() != null && + ticket.getReporter().getId().equals(user.getId())) { + return; + } + } + + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); + } + + private void requireAdmin(User user) { + if (user.getRole() != Role.ADMIN) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); + } } - //findAll (pageable) + private User requireCurrentUser() { + try { + return securityUtils.getCurrentUser(); + } catch (RuntimeException ex) { + + String message = ex.getMessage(); + + boolean authFailure = + "No authenticated user in security context".equals(message) || + "Authenticated user not found in database".equals(message); + + if (authFailure) { + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, + "Authentication required", + ex + ); + } + + throw ex; + } + } + + // ----------------- status logic ----------------- @Transactional public TicketViewDTO updateTicketStatus(Long id, TicketStatus newStatus) { - // TODO: Check role? Is user is Admin or Investigator? -// Typ/Placeholder: -// if (user.getRole() != Role.ADMIN && user.getRole() != Role.INVESTIGATOR) { -// throw new AccessDeniedException("Only admins or investigators can update ticket status"); -// } -// */ + + User user = requireCurrentUser(); Ticket ticket = ticketRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + if (user.getRole() != Role.ADMIN) { + + if (user.getRole() == Role.INVESTIGATOR) { + + if (ticket.getInvestigator() == null || + !ticket.getInvestigator().getId().equals(user.getId())) { + + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); + } + + } else { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); + } + } + + // status logic + TicketStatus oldStatus = ticket.getStatus(); - // No-op check if (oldStatus == newStatus) { return ticketMapper.entityToViewDTO(ticket); } - Set allowedTransitions = ALLOWED_TRANSITIONS.getOrDefault(ticket.getStatus(), Set.of()); + Set allowedTransitions = + ALLOWED_TRANSITIONS.get(ticket.getStatus()); + + if (allowedTransitions == null) { + allowedTransitions = Set.of(); + } if (!allowedTransitions.contains(newStatus)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid ticket status transition: Cannot transition from " + ticket.getStatus() + " to " + newStatus); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Invalid transition from " + ticket.getStatus() + " to " + newStatus + ); } if (newStatus == TicketStatus.IN_PROGRESS && ticket.getInvestigator() == null) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot move ticket to IN_PROGRESS without an assigned investigator"); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Cannot move to IN_PROGRESS without investigator" + ); } ticket.setStatus(newStatus); Ticket savedTicket = ticketRepository.save(ticket); - // TODO: Audit log(-service?), auditLogService.log() -// Typ/Placeholder: -// auditLogService.log(ticket.getId(), user, "STATUS_CHANGED", -// "Status changed from " + oldStatus + " to " + newStatus); - return ticketMapper.entityToViewDTO(savedTicket); } - // Bestäm vilka övergångar/transitions som är tillåtna private static final Map> ALLOWED_TRANSITIONS = Map.of( TicketStatus.OPEN, Set.of(TicketStatus.IN_PROGRESS), TicketStatus.IN_PROGRESS, Set.of(TicketStatus.RESOLVED), - TicketStatus.RESOLVED, Set.of(TicketStatus.CLOSED, TicketStatus.IN_PROGRESS), + TicketStatus.RESOLVED, Set.of(TicketStatus.CLOSED), TicketStatus.CLOSED, Set.of() ); + @Transactional public TicketViewDTO assignInvestigator(Long id, Long investigatorId) { - // TODO Check if user is admin? -// Typ/Placeholder: -// if (user.getRole() != Role.ADMIN) { -// throw new AccessDeniedException("Only admins can assign handlers"); -// } -// */ + + if (investigatorId == null) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Investigator ID is required" + ); + } + + User user = requireCurrentUser(); + requireAdmin(user); Ticket ticket = ticketRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); if (ticket.getInvestigator() != null) { - throw new ResponseStatusException(HttpStatus.CONFLICT, ("Ticket already has an investigator assigned")); + throw new ResponseStatusException( + HttpStatus.CONFLICT, + "Ticket already has an investigator assigned" + ); } if (ticket.getStatus() != TicketStatus.OPEN) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Can only assign investigator to an OPEN ticket, current status: " + ticket.getStatus())); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Ticket must be in OPEN status to assign an investigator" + ); } User investigator = userRepository.findById(investigatorId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Investigator not found")); if (investigator.getRole() != Role.INVESTIGATOR) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("User is not an investigator")); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "User is not an investigator" + ); } ticket.setInvestigator(investigator); ticket.setStatus(TicketStatus.IN_PROGRESS); + Ticket savedTicket = ticketRepository.save(ticket); - // TODO: auditLogService.log() -// Typ/Placeholder: -// auditLogService.log(ticket.getId(), user, "ASSIGNED", -// "Ticket assigned to " + investigatorId); return ticketMapper.entityToViewDTO(savedTicket); } @Transactional public TicketViewDTO unassignInvestigator(Long id) { - // TODO: Check if user is admin? -// Typ/Placeholder: -// if (actor.getRole() != Role.ADMIN) { -// throw new AccessDeniedException("Only admins can unassign handlers"); -// } -// */ + + User user = requireCurrentUser(); + requireAdmin(user); Ticket ticket = ticketRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); if (ticket.getInvestigator() == null) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Ticket does not have an investigator assigned")); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No investigator assigned"); } if (ticket.getStatus() != TicketStatus.IN_PROGRESS) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Can only unassign investigator from an IN_PROGRESS ticket, current status: " + ticket.getStatus())); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Must be IN_PROGRESS"); } ticket.setInvestigator(null); ticket.setStatus(TicketStatus.OPEN); - Ticket savedTicket = ticketRepository.save(ticket); - // TODO: auditLogService.log() -// Typ/Placeholder: -// auditLogService.log(ticket.getId(), user, "UNASSIGNED", -// "Ticket unassigned from " + investigatorId); + Ticket savedTicket = ticketRepository.save(ticket); return ticketMapper.entityToViewDTO(savedTicket); } - -} +} \ No newline at end of file