cred change start

This commit is contained in:
Anthony Stirling 2023-09-03 16:40:40 +01:00
parent 9fe96bec40
commit 146dd3c00b
11 changed files with 275 additions and 30 deletions

View file

@ -13,7 +13,6 @@ public class AppConfig {
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
System.out.println(applicationProperties.toString());
return applicationProperties.getSecurity().getEnableLogin();
}

View file

@ -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> 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);
}
}

View file

@ -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);

View file

@ -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")

View file

@ -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());
}

View file

@ -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<String> 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<User> 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<User> 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<String> 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<User> 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) {

View file

@ -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> 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";
}
}

View file

@ -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<Authority> authorities = new HashSet<>();
@ -50,7 +53,14 @@ public class User {
private Map<String, String> 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;
}

View file

@ -42,7 +42,7 @@ red=Red
green=Green
blue=Blue
custom=Custom...
changeCredsMessage=First time login, Please change your username and/or password!

View file

@ -16,11 +16,14 @@
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
<hr>
<div th:if="${changeCredsFlag}" class="alert alert-success" th:text="#{changeCredsMessage}"></div>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<div th:if="${error}" class="alert alert-danger" role="alert">
<span th:text="${error}">Error Message</span>
</div>
<!-- Change Username Form -->
<h4></h4>
<form action="/change-username" method="post">

View file

@ -0,0 +1,60 @@
<!doctype html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{changeCreds.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
<hr>
<div th:if="${changeCredsFlag}" class="alert alert-success" th:text="#{changeCredsMessage}"></div>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<!-- Change Username Form -->
<h4></h4>
<h4 th:text="#{changeCreds.changeUserAndPassword}">Change Username and password</h4>
<form action="/change-username-and-password" method="post">
<div class="mb-3">
<label for="newUsername" th:text="#{changeCreds.newUsername}">New Username</label>
<input type="text" class="form-control" name="newUsername" id="newUsername" placeholder="New Username">
</div>
<div class="mb-3">
<label for="currentPassword" th:text="#{changeCreds.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{changeCreds.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>