diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index c2b26538..309437b6 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -13,7 +13,6 @@ public class AppConfig { @Bean(name = "loginEnabled") public boolean loginEnabled() { - System.out.println(applicationProperties.toString()); return applicationProperties.getSecurity().getEnableLogin(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java new file mode 100644 index 00000000..8e612464 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -0,0 +1,53 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.User; + +@Component +public class FirstLoginFilter extends OncePerRequestFilter { + + @Autowired + @Lazy + private UserService userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + // Check if the request is for static resources + boolean isStaticResource = requestURI.startsWith("/css/") + || requestURI.startsWith("/js/") + || requestURI.startsWith("/images/") + || requestURI.startsWith("/public/") + || requestURI.endsWith(".svg"); + + // If it's a static resource, just continue the filter chain and skip the logic below + if (isStaticResource) { + filterChain.doFilter(request, response); + return; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + Optional user = userService.findByUsername(authentication.getName()); + if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { + response.sendRedirect("/change-creds"); + return; + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 159b8b03..7097020b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -28,7 +28,7 @@ public class InitialSecuritySetup { if (!userService.hasUsers()) { String initialUsername = "admin"; String initialPassword = "stirling"; - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index e0afcb9a..dfe782ac 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -41,6 +41,9 @@ public class SecurityConfiguration { @Autowired private UserAuthenticationFilter userAuthenticationFilter; + @Autowired + private FirstLoginFilter firstLoginFilter; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -48,6 +51,7 @@ public class SecurityConfiguration { if(loginEnabledValue) { http.csrf(csrf -> csrf.disable()); + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); http .formLogin(formLogin -> formLogin .loginPage("/login") diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 19bf007b..46c5aeff 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -113,12 +113,23 @@ public class UserService { userRepository.save(user); } + public void saveUser(String username, String password, String role, boolean firstLogin) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + user.setFirstLogin(firstLogin); + userRepository.save(user); + } + public void saveUser(String username, String password, String role) { User user = new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); user.addAuthority(new Authority(role, user)); user.setEnabled(true); + user.setFirstLogin(false); userRepository.save(user); } @@ -168,6 +179,12 @@ public class UserService { userRepository.save(user); } + public void changeFirstUse(User user, boolean firstUse) { + user.setFirstLogin(firstUse); + userRepository.save(user); + } + + public boolean isPasswordCorrect(User user, String currentPassword) { return passwordEncoder.matches(currentPassword, user.getPassword()); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 9eaa05bd..f232daf1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -16,6 +16,8 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -39,25 +41,80 @@ public class UserController { return "redirect:/login?registered=true"; } - @PostMapping("/change-username") - public ResponseEntity changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) { + @PostMapping("/change-username-and-password") + public RedirectView changeUsernameAndPassword(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { if (principal == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/error"); } - + Optional userOpt = userService.findByUsername(principal.getName()); - - if(userOpt == null || userOpt.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/error"); } User user = userOpt.get(); - - if(!userService.isPasswordCorrect(user, currentPassword)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/error"); } - - if(userService.usernameExists(newUsername)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists."); + + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + redirectAttributes.addFlashAttribute("error", "New username already exists."); + return new RedirectView("/error"); + } + + userService.changePassword(user, newPassword); + if(!user.getUsername().equals(newUsername)) { + userService.changeUsername(user, newUsername); + } + userService.changeFirstUse(user, false); + + // Logout using Spring's utility + new SecurityContextLogoutHandler().logout(request, response, null); + + redirectAttributes.addFlashAttribute("credsUpdated", true); + return new RedirectView("/login"); + } + + + + @PostMapping("/change-username") + public RedirectView changeUsername(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/account"); + } + + Optional userOpt = userService.findByUsername(principal.getName()); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/account"); + } + User user = userOpt.get(); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/account"); + } + + if (userService.usernameExists(newUsername)) { + redirectAttributes.addFlashAttribute("error", "New username already exists."); + return new RedirectView("/account"); } userService.changeUsername(user, newUsername); @@ -65,33 +122,44 @@ public class UserController { // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - - return ResponseEntity.ok("Username updated successfully."); + redirectAttributes.addFlashAttribute("message", "Username updated successfully."); + return new RedirectView("/login"); } @PostMapping("/change-password") - public ResponseEntity changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) { + public RedirectView changePassword(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { if (principal == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/account"); } Optional userOpt = userService.findByUsername(principal.getName()); - - if(userOpt == null || userOpt.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/account"); } User user = userOpt.get(); - if(!userService.isPasswordCorrect(user, currentPassword)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/account"); } userService.changePassword(user, newPassword); // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - - return ResponseEntity.ok("Password updated successfully."); + + redirectAttributes.addFlashAttribute("message", "Password updated successfully."); + return new RedirectView("/login"); } + @PostMapping("/updateUserSettings") public String updateUserSettings(HttpServletRequest request, Principal principal) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index b86e6e86..cba7ebb5 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -91,6 +91,7 @@ public class AccountWebController { model.addAttribute("username", username); model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("settings", settingsJson); + model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); } } else { return "redirect:/"; @@ -100,5 +101,35 @@ public class AccountWebController { + @GetMapping("/change-creds") + public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/"; + } + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; + + // Retrieve username and other attributes + String username = userDetails.getUsername(); + + // Fetch user details from the database + Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } + // Add attributes to the model + model.addAttribute("username", username); + } + } else { + return "redirect:/"; + } + return "change-creds"; + } + } diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index 2881e308..f771a821 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -40,6 +40,9 @@ public class User { @Column(name = "enabled") private boolean enabled; + @Column(name = "isFirstLogin") + private Boolean isFirstLogin = false; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") private Set authorities = new HashSet<>(); @@ -50,7 +53,14 @@ public class User { private Map settings = new HashMap<>(); // Key-value pairs of settings. - + public boolean isFirstLogin() { + return isFirstLogin != null && isFirstLogin; + } + + public void setFirstLogin(boolean isFirstLogin) { + this.isFirstLogin = isFirstLogin; + } + public Long getId() { return id; } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 03362eec..a5620aa1 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -42,7 +42,7 @@ red=Red green=Green blue=Blue custom=Custom... - +changeCredsMessage=First time login, Please change your username and/or password! diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 5a4882e5..ba104b86 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -16,11 +16,14 @@

User Settings


- +
+

User!

- +

diff --git a/src/main/resources/templates/change-creds.html b/src/main/resources/templates/change-creds.html new file mode 100644 index 00000000..c6d94c66 --- /dev/null +++ b/src/main/resources/templates/change-creds.html @@ -0,0 +1,60 @@ + + + + + + + +
+
+
+

+
+
+
+ + +

User Settings

+
+
+ + +

User!

+ + + +

+

Change Username and password

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