diff --git a/src/main/java/org/example/visacasemanagementsystem/config/OauthSuccessHandler.java b/src/main/java/org/example/visacasemanagementsystem/config/OauthSuccessHandler.java index 0dd0a6d..edf45c0 100644 --- a/src/main/java/org/example/visacasemanagementsystem/config/OauthSuccessHandler.java +++ b/src/main/java/org/example/visacasemanagementsystem/config/OauthSuccessHandler.java @@ -3,35 +3,31 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.example.visacasemanagementsystem.user.UserAuthorization; import org.example.visacasemanagementsystem.user.entity.User; -import org.example.visacasemanagementsystem.user.repository.UserRepository; import org.example.visacasemanagementsystem.user.security.UserPrincipal; +import org.example.visacasemanagementsystem.user.service.UserService; import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.context.SecurityContextRepository; + import java.io.IOException; import java.util.Objects; -import java.util.UUID; @Configuration public class OauthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; + private final UserService userService; private final SecurityContextRepository securityContextRepository; - public OauthSuccessHandler(UserRepository userRepository, PasswordEncoder passwordEncoder, SecurityContextRepository securityContextRepository) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; + public OauthSuccessHandler(UserService userService, + SecurityContextRepository securityContextRepository) { + this.userService = userService; this.securityContextRepository = securityContextRepository; } @@ -49,19 +45,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, throw new BadCredentialsException("OAuth account missing verified email"); } - User user = userRepository.findByEmail(email).orElseGet(() -> { - User newUser = new User(); - newUser.setFullName(name); - newUser.setUsername(email); - newUser.setEmail(email); - newUser.setPassword(passwordEncoder.encode(UUID.randomUUID().toString())); - newUser.setUserAuthorization(UserAuthorization.USER); - try { - return userRepository.saveAndFlush(newUser); - } catch (DataIntegrityViolationException e) { - return userRepository.findByEmail(email).orElseThrow(); - } - }); + // Persistence + audit run inside a @Transactional service method so the + // transaction commits before we touch the SecurityContext or issue the redirect. + User user = userService.findOrCreateOauthUser(email, name); UserPrincipal principal = new UserPrincipal(user); Authentication auth = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); @@ -72,7 +58,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, clearAuthenticationAttributes(request); - String targetUrl = "/dashboard"; + String targetUrl = "/home"; getRedirectStrategy().sendRedirect(request, response, targetUrl); } } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java b/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java index 8afd999..e294e3f 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java @@ -59,8 +59,12 @@ public String userSignupForm(@AuthenticationPrincipal UserPrincipal principal, M public String createUser(@RequestParam String fullName, @RequestParam String email, @RequestParam String password, + @RequestParam String confirmPassword, Model model) { try { + if (!password.equals(confirmPassword)) { + throw new IllegalArgumentException("Passwords do not match"); + } CreateUserDTO dto = new CreateUserDTO(fullName, email, password, UserAuthorization.USER); userService.createUser(dto); return "redirect:/user/login"; @@ -144,12 +148,19 @@ public String showProfileEditForm(@AuthenticationPrincipal UserPrincipal princip public String updateProfile(@AuthenticationPrincipal UserPrincipal principal, @PathVariable Long userId, @RequestParam String fullName, - @RequestParam String email, + @RequestParam String password, + @RequestParam String confirmPassword, Model model) { userService.validateProfileAccess(principal, userId); try { - UpdateUserDTO dto = new UpdateUserDTO(userId, fullName, email); + if (!password.equals(confirmPassword)) { + throw new IllegalArgumentException("Passwords do not match"); + } + if (!password.isBlank() && password.length() < 8) { + throw new IllegalArgumentException("Password must be at least 8 characters"); + } + UpdateUserDTO dto = new UpdateUserDTO(userId, fullName, password); userService.updateUser(dto, principal.getUserId()); return "redirect:/profile/view/" + userId; } catch (IllegalArgumentException e) { @@ -158,7 +169,7 @@ public String updateProfile(@AuthenticationPrincipal UserPrincipal principal, UserDTO existing = userService.findById(userId) .orElseThrow(() -> new EntityNotFoundException("User not found")); model.addAttribute("error", e.getMessage()); - model.addAttribute("user", new UserDTO(userId, fullName, email, existing.userAuthorization())); + model.addAttribute("user", new UserDTO(userId, fullName, existing.email(), existing.userAuthorization())); addAuthorizationFormAttributes(model, principal, userId); return "profile/edit"; } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/dto/UpdateUserDTO.java b/src/main/java/org/example/visacasemanagementsystem/user/dto/UpdateUserDTO.java index 11cb128..390fbc5 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/dto/UpdateUserDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/dto/UpdateUserDTO.java @@ -2,9 +2,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; public record UpdateUserDTO( @NotNull Long id, @NotBlank(message = "Full name must be specified") String fullName, - @NotBlank(message = "Email must be specified") String email) { + @NotNull(message = "Password must be provided (use empty string to skip change)") + @Pattern(regexp = "^$|.{8,}", message = "Password must be at least 8 characters") String password) { } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java b/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java index 881cd9c..5d10770 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java @@ -38,7 +38,5 @@ public void updateEntityFromDTO(UpdateUserDTO dto, User user) { if (dto == null || user == null) return; user.setFullName(dto.fullName()); - user.setEmail(dto.email()); - user.setUsername(dto.email()); } } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java b/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java index 08f53e8..afac408 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java @@ -15,12 +15,14 @@ import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.UUID; @PreAuthorize("isAuthenticated()") @Service @@ -89,26 +91,64 @@ public UserDTO createUser(@Valid CreateUserDTO dto) { @Transactional public UserDTO updateUser(UpdateUserDTO dto, Long actorUserId) { + validateProfileAccess(getUserPrincipal(), dto.id()); // Check if user and email exists User user = userRepository.findById(dto.id()) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + if (!dto.password().isBlank()) { + user.setPassword(passwordEncoder.encode(dto.password())); + } + User savedUser; try { userMapper.updateEntityFromDTO(dto, user); savedUser = userRepository.saveAndFlush(user); } catch (DataIntegrityViolationException e) { - throw new IllegalArgumentException("A user with this email already exists", e); + // Email is no longer mutable on this path, but other unique/integrity constraints + // (now or in the future) could still trip here, so the message stays generic. + throw new IllegalArgumentException("Data integrity violation while updating user", e); } userLogService.createUserLog( actorUserId, savedUser.getId(), UserEventType.UPDATED, - "User profile updated (fullName/email)." + "User profile updated (fullName/password)." ); return userMapper.toDTO(savedUser); } + /** + * OAuth login lookup-or-create. Used by OauthSuccessHandler to keep the persistence work + * inside a single short-lived transaction that commits before the success handler builds + * the SecurityContext and issues the redirect. + */ + @PreAuthorize("permitAll()") + @Transactional + public User findOrCreateOauthUser(String email, String fullName) { + return userRepository.findByEmail(email).orElseGet(() -> { + User newUser = new User(); + newUser.setFullName(fullName); + newUser.setUsername(email); + newUser.setEmail(email); + newUser.setPassword(passwordEncoder.encode(UUID.randomUUID().toString())); + newUser.setUserAuthorization(UserAuthorization.USER); + try { + User savedUser = userRepository.saveAndFlush(newUser); + userLogService.createUserLog( + savedUser.getId(), + savedUser.getId(), + UserEventType.CREATED, + "User account created via OAUTH2." + ); + return savedUser; + } catch (DataIntegrityViolationException e) { + // Race: another concurrent OAuth login created the row first. Re-read. + return userRepository.findByEmail(email).orElseThrow(); + } + }); + } + @PreAuthorize("hasRole('SYSADMIN')") @Transactional public UserDTO updateUserAuthorization(Long actorUserId, Long targetUserId, UserAuthorization newAuth) { @@ -154,4 +194,15 @@ public void validateProfileAccess(UserPrincipal principal, Long userId) { throw new UnauthorizedException("You do not have permission to edit this profile."); } } + + private static UserPrincipal getUserPrincipal() { + // Class-level @PreAuthorize("isAuthenticated()") guarantees an authentication is present. + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (!(principal instanceof UserPrincipal userPrincipal)) { + throw new IllegalStateException( + "Expected UserPrincipal in SecurityContext but got: " + + (principal == null ? "null" : principal.getClass().getName())); + } + return userPrincipal; + } } diff --git a/src/main/resources/templates/profile/edit.html b/src/main/resources/templates/profile/edit.html index b41393d..562cc75 100644 --- a/src/main/resources/templates/profile/edit.html +++ b/src/main/resources/templates/profile/edit.html @@ -47,11 +47,16 @@

Edit Profile

required>
- - + + +
+
+ +