Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
61dc4bc
add: startPage should not be filtered
FionaSprinkles Apr 15, 2026
d80add6
add: startPage to permitted endpoints
FionaSprinkles Apr 15, 2026
a47fe70
Create StartPageController and add initial code
FionaSprinkles Apr 15, 2026
bfa108e
Correct path to login view
FionaSprinkles Apr 15, 2026
62d5ff3
add : layout to login
FionaSprinkles Apr 15, 2026
53813f8
add : token login and box styling
FionaSprinkles Apr 15, 2026
b7be9cd
Create startPage
FionaSprinkles Apr 15, 2026
8676421
Permit access to root endpoint
FionaSprinkles Apr 15, 2026
9f12a28
Update navigation links and make logo clickable
FionaSprinkles Apr 15, 2026
2b74474
Create preview before submitting ticket
FionaSprinkles Apr 15, 2026
77b05eb
add: ticket preview method
FionaSprinkles Apr 15, 2026
0948b89
update endpoint to preview
FionaSprinkles Apr 15, 2026
fee34fb
permit access to ticket create and preview endpoints
FionaSprinkles Apr 15, 2026
5e4912a
feat: add anonymous and authenticated ticket submission endpoints
FionaSprinkles Apr 15, 2026
1512997
feat: add preview page actions for anonymous and authenticated submis…
FionaSprinkles Apr 15, 2026
b9c3cf1
refactor: move token generation from entity to service
FionaSprinkles Apr 15, 2026
6e34d83
add: method for anonymous ticket
FionaSprinkles Apr 15, 2026
419a420
add: token field
FionaSprinkles Apr 15, 2026
6b7eed6
feat(auth): add user feedback for login, logout and token validation
addee1 Apr 16, 2026
304ea31
adds success message if ticket created successfully
addee1 Apr 16, 2026
a412f14
feat(global): add isLoggedIn to all views via ControllerAdvice
addee1 Apr 16, 2026
817804a
feat(layout): add conditional navbar and success toast
addee1 Apr 16, 2026
bf89d3b
feat(login): add token form and error handling
addee1 Apr 16, 2026
27ef419
feat(tickets): add my tickets view
addee1 Apr 16, 2026
c1a85b7
feat(tickets): pass success message to preview page
addee1 Apr 16, 2026
c7eb1b7
fix(security): allow access to logout endpoint
addee1 Apr 16, 2026
777313c
style(auth): improve signup page layout and styling
addee1 Apr 16, 2026
264bb62
feat(home): enable success toast on start page
addee1 Apr 16, 2026
6f37340
Add Model parameter to start page handler
addee1 Apr 16, 2026
bac8b3d
Adds styling
addee1 Apr 16, 2026
0d97015
Change reportToken to have nullable true
addee1 Apr 16, 2026
b4dfd49
feat(tickets): add ticket-created page with token display
addee1 Apr 16, 2026
01ae946
feat(tickets): improve ticket creation flow with success feedback and…
addee1 Apr 16, 2026
44ce296
feat(tickets): support anonymous ticket creation with reporter token
addee1 Apr 16, 2026
0b784ee
feat(tickets): add formatted createdAt helper in TicketViewDTO
addee1 Apr 16, 2026
fa034bd
add success toast support to ticket view page
addee1 Apr 16, 2026
c55e12a
feat: improve create ticket UX based on login state
addee1 Apr 16, 2026
7b38dd3
fix(tickets): handle anonymous user safely in ticket creation
addee1 Apr 16, 2026
6eed49a
Merge remote-tracking branch 'refs/remotes/origin/main' into feature/…
simonforsberg Apr 16, 2026
4486ddf
Refactor `TicketServiceTest` to use nested class for `createNewTicket…
simonforsberg Apr 16, 2026
0230126
Add verification that `getMyTickets` does not call repository for una…
simonforsberg Apr 16, 2026
cc4e5c9
Enhance `createNewTicket` tests in `TicketServiceTest` by adding asse…
simonforsberg Apr 16, 2026
33cfc79
fix: narrow exception handling in GlobalModelAttributes
addee1 Apr 16, 2026
9c0b3d0
fix: only return null for auth failures in getCurrentUserOrNull
addee1 Apr 16, 2026
2ee1dc1
fix: encode token in URL to prevent invalid routing
addee1 Apr 16, 2026
2ba8839
fix: remove duplicate JS toast animation and rely on CSS
addee1 Apr 16, 2026
d5b8315
fix: remove absolute security claim in UI text
addee1 Apr 16, 2026
c21f2d1
fix: include token in TicketViewDTO for token-based access
addee1 Apr 16, 2026
089b0e8
style: fix CSS lint issues (import syntax, font-family, keyframes nam…
addee1 Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/main/java/org/example/alfs/config/GlobalModelAttributes.java
Original file line number Diff line number Diff line change
@@ -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);
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
model.addAttribute("isLoggedIn", isLoggedIn);
model.addAttribute("username", username);
}
}
6 changes: 5 additions & 1 deletion src/main/java/org/example/alfs/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
50 changes: 31 additions & 19 deletions src/main/java/org/example/alfs/controllers/AuthViewController.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
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;
import org.example.alfs.entities.User;
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).
*
Expand All @@ -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")
Expand Down Expand Up @@ -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) {

Expand All @@ -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:/";
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
51 changes: 40 additions & 11 deletions src/main/java/org/example/alfs/controllers/TicketController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/org/example/alfs/dto/ticket/TicketViewDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import lombok.Data;
import org.example.alfs.enums.TicketStatus;

import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;

/*
Expand All @@ -17,10 +17,17 @@
public class TicketViewDTO {

private Long id;
private String token;
Comment thread
addee1 marked this conversation as resolved.
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"));
}
}
4 changes: 1 addition & 3 deletions src/main/java/org/example/alfs/entities/Ticket.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}


Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/example/alfs/security/SecurityUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
addee1 marked this conversation as resolved.
}
}
35 changes: 27 additions & 8 deletions src/main/java/org/example/alfs/services/TicketService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading