diff --git a/src/main/java/org/example/alfs/config/GlobalModelAttributes.java b/src/main/java/org/example/alfs/config/GlobalModelAttributes.java new file mode 100644 index 0000000..cf9cf9d --- /dev/null +++ b/src/main/java/org/example/alfs/config/GlobalModelAttributes.java @@ -0,0 +1,35 @@ +package org.example.alfs.config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.example.alfs.security.SecurityUtils; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ModelAttribute; + +@ControllerAdvice +public class GlobalModelAttributes { + private static final Logger log = LoggerFactory.getLogger(GlobalModelAttributes.class); + private final SecurityUtils securityUtils; + + public GlobalModelAttributes(SecurityUtils securityUtils) { + this.securityUtils = securityUtils; + } + + @ModelAttribute + public void addGlobalAttributes(Model model) { + + boolean isLoggedIn = false; + String username = null; + + try { + var user = securityUtils.getCurrentUser(); + isLoggedIn = true; + username = user.getUsername(); + } catch (RuntimeException ex) { + log.debug("Could not resolve current user for global model attributes", ex); + } + + model.addAttribute("isLoggedIn", isLoggedIn); + model.addAttribute("username", username); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/config/SecurityConfig.java b/src/main/java/org/example/alfs/config/SecurityConfig.java index 0b61490..e2565e7 100644 --- a/src/main/java/org/example/alfs/config/SecurityConfig.java +++ b/src/main/java/org/example/alfs/config/SecurityConfig.java @@ -41,11 +41,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login").permitAll() .requestMatchers("/auth/signup").permitAll() + .requestMatchers("/auth/logout").permitAll() .requestMatchers("/auth/hash").permitAll() .requestMatchers("/h2-console/**").permitAll() + .requestMatchers("/startPage", "/").permitAll() + .requestMatchers("/tickets/create").permitAll() + .requestMatchers("/tickets/previewTicket").permitAll() //allow access to endpoints during development - .requestMatchers("/create", "/tickets/**", "/view/**").permitAll() + .requestMatchers("/tickets/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/static/**").permitAll() .requestMatchers("/login", "/login-form").permitAll() .requestMatchers("/signup", "/signup-form").permitAll() diff --git a/src/main/java/org/example/alfs/controllers/AuthViewController.java b/src/main/java/org/example/alfs/controllers/AuthViewController.java index 990cea0..5713b82 100644 --- a/src/main/java/org/example/alfs/controllers/AuthViewController.java +++ b/src/main/java/org/example/alfs/controllers/AuthViewController.java @@ -1,6 +1,6 @@ package org.example.alfs.controllers; -import jakarta.servlet.http.Cookie; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.example.alfs.dto.auth.SignupRequestDTO; @@ -8,11 +8,15 @@ import org.example.alfs.security.JwtService; import org.example.alfs.services.AuthService; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; +import java.time.Duration; + /** * Handles login for the browser (UI). * @@ -32,8 +36,10 @@ public AuthViewController(AuthService authService, JwtService jwtService) { } @GetMapping("/login") - public String loginPage() { - return "login"; // login.jte + public String loginPage(@RequestParam(required = false) String error, @RequestParam(required = false) String tokenError, Model model) { + model.addAttribute("error", error); + model.addAttribute("tokenError", tokenError); + return "login"; } @GetMapping("/signup") @@ -61,21 +67,25 @@ public String signupForm( public String loginForm( @RequestParam String username, @RequestParam String password, - HttpServletResponse response + HttpServletResponse response, + RedirectAttributes redirectAttributes ) { try { User user = authService.login(username, password); String token = jwtService.generateToken(user); - Cookie cookie = new Cookie("JWT", token); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(60 * 60 * 24); + ResponseCookie cookie = ResponseCookie.from("JWT", token) + .httpOnly(true) + .path("/") + .maxAge(Duration.ofDays(1)) + .sameSite("Lax") + .build(); - response.addCookie(cookie); + response.addHeader("Set-Cookie", cookie.toString()); - return "redirect:/api/hello"; // should change later + redirectAttributes.addFlashAttribute("success", "You are signed in!"); + return "redirect:/"; } catch (ResponseStatusException ex) { @@ -89,16 +99,18 @@ public String loginForm( } } - @PostMapping("/logout") - public String logout(HttpServletResponse response) { - - Cookie cookie = new Cookie("JWT", null); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); + @PostMapping("/auth/logout") + public String logout(HttpServletResponse response, RedirectAttributes redirectAttributes) { - response.addCookie(cookie); + ResponseCookie cookie = ResponseCookie.from("JWT", "") + .httpOnly(true) + .path("/") + .maxAge(Duration.ZERO) + .sameSite("Lax") + .build(); - return "redirect:/login"; + response.addHeader("Set-Cookie", cookie.toString()); + redirectAttributes.addFlashAttribute("success", "Successfully signed out"); + return "redirect:/"; } } \ No newline at end of file diff --git a/src/main/java/org/example/alfs/controllers/StartPageController.java b/src/main/java/org/example/alfs/controllers/StartPageController.java new file mode 100644 index 0000000..a41a840 --- /dev/null +++ b/src/main/java/org/example/alfs/controllers/StartPageController.java @@ -0,0 +1,14 @@ +package org.example.alfs.controllers; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class StartPageController { + + @GetMapping("/") + public String startPage(Model model){ + return "startPage"; + } +} diff --git a/src/main/java/org/example/alfs/controllers/TicketController.java b/src/main/java/org/example/alfs/controllers/TicketController.java index 7ff0a32..0701b52 100644 --- a/src/main/java/org/example/alfs/controllers/TicketController.java +++ b/src/main/java/org/example/alfs/controllers/TicketController.java @@ -1,5 +1,7 @@ package org.example.alfs.controllers; - +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.validation.Valid; import org.example.alfs.dto.ticket.TicketAssignDTO; import org.example.alfs.dto.ticket.TicketCreateDTO; @@ -27,36 +29,56 @@ public TicketController(TicketService ticketService) { } //create ticket - @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access + //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @GetMapping("/create") public String createNewTicketForm(Model model) { model.addAttribute("ticket", new TicketCreateDTO()); return "create"; } - @PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access + //@PreAuthorize("hasRole('REPORTER')") // should change later for anonymous access @PostMapping("/create") - public String createNewTicket(@ModelAttribute("ticket") @Valid TicketCreateDTO ticketCreateDTO, BindingResult bindingResult, Model model) { + public String createNewTicket( + @ModelAttribute("ticket") @Valid TicketCreateDTO dto, + BindingResult bindingResult, + Model model, + RedirectAttributes redirectAttributes + ) { if (bindingResult.hasErrors()) { - model.addAttribute("ticket", ticketCreateDTO); + model.addAttribute("ticket", dto); return "create"; } - TicketViewDTO ticket = ticketService.createNewTicket(ticketCreateDTO); + TicketViewDTO ticket = ticketService.createNewTicket(dto); + redirectAttributes.addFlashAttribute("success", "Ticket created successfully"); - return "redirect:/tickets/" + ticket.getId(); + if (ticket.getToken() != null) { + return "redirect:/tickets/ticket-created?token=" + ticket.getToken(); + } + return "redirect:/tickets/" + ticket.getId(); } - //view ticket by token + + + //view ticket by token @GetMapping("/token/{token}") public String viewTicketByToken(@PathVariable String token, Model model) { - TicketViewDTO ticket = ticketService.getTicketByToken(token); - model.addAttribute("ticket", ticket); + try { + TicketViewDTO ticket = ticketService.getTicketByToken(token); + model.addAttribute("ticket", ticket); + return "view"; - return "view"; + } catch (ResponseStatusException ex) { + + if (ex.getStatusCode() == HttpStatus.NOT_FOUND) { + return "redirect:/login?tokenError=true"; + } + + throw ex; + } } //view ticket by id @@ -108,6 +130,13 @@ public String myAssignedTickets(Model model) { return "assigned-tickets"; } + + @GetMapping("/ticket-created") + public String ticketCreated(@RequestParam String token, Model model) { + model.addAttribute("token", token); + return "ticket-created"; + } + //create comment //View comment //upload attachment diff --git a/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java b/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java index 2b1108e..d84539f 100644 --- a/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java +++ b/src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java @@ -2,7 +2,7 @@ import lombok.Data; import org.example.alfs.enums.TicketStatus; - +import java.time.format.DateTimeFormatter; import java.time.LocalDateTime; /* @@ -17,10 +17,17 @@ public class TicketViewDTO { private Long id; + private String token; private String title; private String description; private TicketStatus status; private LocalDateTime createdAt; private Long assignedInvestigatorId; + + public String getFormattedCreatedAt() { + if (createdAt == null) return ""; + + return createdAt.format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")); + } } diff --git a/src/main/java/org/example/alfs/entities/Ticket.java b/src/main/java/org/example/alfs/entities/Ticket.java index b159aaa..6db2207 100644 --- a/src/main/java/org/example/alfs/entities/Ticket.java +++ b/src/main/java/org/example/alfs/entities/Ticket.java @@ -36,7 +36,7 @@ public class Ticket { @Column(nullable = false, length = 32) private TicketStatus status; - @Column(nullable = false, unique = true, length = 128, updatable = false) + @Column(nullable = true, unique = true, length = 128, updatable = false) private String reporterToken; private LocalDateTime createdAt; @@ -47,8 +47,6 @@ public class Ticket { public void prePersist() { createdAt = LocalDateTime.now(); if (status == null) status = TicketStatus.OPEN; - if (reporterToken == null || reporterToken.isBlank()) - reporterToken = UUID.randomUUID().toString(); // Skapa token för anonyma anmälare } @PreUpdate diff --git a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java index 274d89f..a1d8e44 100644 --- a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java @@ -26,11 +26,11 @@ public JwtAuthenticationFilter(JwtService jwtService, UserRepository userReposit this.userRepository = userRepository; } - // Skips filter for LOGIN & H2 + // Skips filter for LOGIN & H2 & START PAGE @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); - return path.startsWith("/auth") || path.startsWith("/h2-console"); + return path.startsWith("/auth") || path.startsWith("/h2-console") || path.startsWith("/startPage"); } diff --git a/src/main/java/org/example/alfs/security/SecurityUtils.java b/src/main/java/org/example/alfs/security/SecurityUtils.java index b2ed239..9d76be1 100644 --- a/src/main/java/org/example/alfs/security/SecurityUtils.java +++ b/src/main/java/org/example/alfs/security/SecurityUtils.java @@ -27,4 +27,23 @@ public User getCurrentUser() { return userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("Authenticated user not found in database")); } + + public User getCurrentUserOrNull() { + try { + return 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) { + return null; + } + + throw ex; + } + } } \ No newline at end of file diff --git a/src/main/java/org/example/alfs/services/TicketService.java b/src/main/java/org/example/alfs/services/TicketService.java index 27ee9bb..6114dfe 100644 --- a/src/main/java/org/example/alfs/services/TicketService.java +++ b/src/main/java/org/example/alfs/services/TicketService.java @@ -38,27 +38,46 @@ public TicketService(TicketRepository ticketRepository, } //createNewTicket - public TicketViewDTO createNewTicket(TicketCreateDTO ticketCreateDTO) { + public TicketViewDTO createNewTicket(TicketCreateDTO dto) { Ticket ticket = new Ticket(); - ticket.setTitle(ticketCreateDTO.getTitle()); - ticket.setDescription(ticketCreateDTO.getDescription()); + ticket.setTitle(dto.getTitle()); + ticket.setDescription(dto.getDescription()); - User user = requireCurrentUser(); - ticket.setReporter(user); + User user = securityUtils.getCurrentUserOrNull(); - Ticket savedTicket = ticketRepository.save(ticket); + String token = null; - return ticketMapper.entityToViewDTO(savedTicket); + if (user != null) { + ticket.setReporter(user); + } else { + token = java.util.UUID.randomUUID().toString(); + ticket.setReporterToken(token); + } + + Ticket saved = ticketRepository.save(ticket); + + TicketViewDTO view = ticketMapper.entityToViewDTO(saved); + + if (token != null) { + view.setToken(token); + } + + return view; } + // View by token public TicketViewDTO getTicketByToken(String token) { Ticket ticket = ticketRepository.findByReporterToken(token) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); - return ticketMapper.entityToViewDTO(ticket); + TicketViewDTO view = ticketMapper.entityToViewDTO(ticket); + + view.setToken(ticket.getReporterToken()); + + return view; } //findById diff --git a/src/main/jte/create.jte b/src/main/jte/create.jte index 02fc472..0441912 100644 --- a/src/main/jte/create.jte +++ b/src/main/jte/create.jte @@ -1,8 +1,20 @@ @param org.example.alfs.dto.ticket.TicketCreateDTO ticket - +@param String success +@param Boolean isLoggedIn +@param String username @template.layout(title = "Create Ticket", content = @`