diff --git a/src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java b/src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java index baab382..793ff18 100644 --- a/src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java @@ -1,16 +1,15 @@ package org.example.visacasemanagementsystem; -import org.example.visacasemanagementsystem.user.security.SecurityUser; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; - import java.util.Objects; @Controller public class ApplicationViewController { @GetMapping("/dashboard") - public String dashboard(@AuthenticationPrincipal SecurityUser principal) { + public String dashboard(@AuthenticationPrincipal UserPrincipal principal) { if (principal == null) { return "redirect:/user/login"; } diff --git a/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java b/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java index f98fc6b..82d11ea 100644 --- a/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java +++ b/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java @@ -9,4 +9,5 @@ public class VisaCaseManagementSystemApplication { public static void main(String[] args) { SpringApplication.run(VisaCaseManagementSystemApplication.class, args); } + } diff --git a/src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java b/src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java index 6db21ed..23b5d2e 100644 --- a/src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java +++ b/src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java @@ -6,37 +6,43 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class DataInitializer{ @Bean - public CommandLineRunner initData(UserRepository userRepository) { + public CommandLineRunner initData(UserRepository userRepository, PasswordEncoder passwordEncoder) { return args -> { if (userRepository.findByEmail("user@test.com").isEmpty()) { User user = new User(); user.setFullName("USER"); + user.setUsername("user@test.com"); user.setEmail("user@test.com"); - user.setPassword("password"); + user.setPassword(passwordEncoder.encode("password")); user.setUserAuthorization(UserAuthorization.USER); userRepository.save(user); System.out.println("Testanvändare skapad med ID: " + user.getId()); + } + if (userRepository.findByEmail("admin@test.com").isEmpty()) { User admin = new User(); admin.setFullName("ADMIN"); - admin.setEmail("user@test.com2"); - admin.setPassword("password"); + admin.setUsername("admin@test.com"); + admin.setEmail("admin@test.com"); + admin.setPassword(passwordEncoder.encode("password")); admin.setUserAuthorization(UserAuthorization.ADMIN); userRepository.save(admin); System.out.println("Test-admin skapad med ID: " + admin.getId()); } - if (userRepository.findByEmail("admin@test.com").isEmpty()) { + if (userRepository.findByEmail("sysadmin@test.com").isEmpty()) { User sysadmin = new User(); sysadmin.setFullName("SYSTEM ADMIN"); - sysadmin.setEmail("admin@test.com"); - sysadmin.setPassword("password"); + sysadmin.setUsername("sysadmin@test.com"); + sysadmin.setEmail("sysadmin@test.com"); + sysadmin.setPassword(passwordEncoder.encode("password")); sysadmin.setUserAuthorization(UserAuthorization.SYSADMIN); userRepository.save(sysadmin); System.out.println("Test-sysadmin skapad med ID: " + sysadmin.getId()); diff --git a/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java index 0ed23c5..f0d6685 100644 --- a/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java +++ b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java @@ -2,19 +2,60 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; @Configuration +@EnableWebSecurity +@EnableMethodSecurity public class SecurityConfig { + private final UserDetailsService userDetailsService; + + public SecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() + .requestMatchers("/user/signup").permitAll() + .requestMatchers("/user/login").permitAll() + .requestMatchers("/dashboard").authenticated() + .requestMatchers("/**/admin").hasRole("ADMIN") + .requestMatchers("/**/applicant").hasRole("USER") + //TODO: requestMatchers for /**/{userId} endpoints, etc. + .anyRequest().hasRole("SYSADMIN") ) - .headers(headers -> headers.frameOptions(frame -> frame.disable())); + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + .formLogin(l -> l + .defaultSuccessUrl("/dashboard", true) + .loginPage("/user/login")) + .logout(withDefaults()) //TODO: Custom logout page required + .httpBasic(withDefaults()); + return http.build(); } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authnProvider = new DaoAuthenticationProvider(userDetailsService); + authnProvider.setPasswordEncoder(passwordEncoder()); + return authnProvider; + } + + @Bean + public static PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java b/src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java index 4ea1096..884b52b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java @@ -3,5 +3,9 @@ public enum UserAuthorization { USER, ADMIN, - SYSADMIN + SYSADMIN; + + public String asAuthority() { + return "ROLE_" + name(); + } } 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 5f5066b..362dd2d 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java @@ -7,7 +7,7 @@ import org.example.visacasemanagementsystem.user.dto.CreateUserDTO; import org.example.visacasemanagementsystem.user.dto.UpdateUserDTO; import org.example.visacasemanagementsystem.user.dto.UserDTO; -import org.example.visacasemanagementsystem.user.security.SecurityUser; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.example.visacasemanagementsystem.user.service.UserService; import org.example.visacasemanagementsystem.visa.dto.VisaDTO; import org.example.visacasemanagementsystem.visa.service.VisaService; @@ -18,7 +18,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; - import java.util.List; import java.util.Objects; @@ -71,7 +70,7 @@ public String userLoginForm(){ // Uneditable profile view from where the user themselves or a sysadmin can access the profile edit view through a // button only available to them @GetMapping("/profile/view/{userId}") - public String viewProfile(@AuthenticationPrincipal SecurityUser principal, + public String viewProfile(@AuthenticationPrincipal UserPrincipal principal, @PathVariable Long userId, Model model) { UserDTO user = userService.findById(userId) @@ -90,7 +89,7 @@ public String viewProfile(@AuthenticationPrincipal SecurityUser principal, // Form for editing a users information, only available to the user themselves and sysadmins @GetMapping("/profile/edit/{userId}") - public String showProfileEditForm(@AuthenticationPrincipal SecurityUser principal, + public String showProfileEditForm(@AuthenticationPrincipal UserPrincipal principal, @PathVariable Long userId, Model model) { userService.validateProfileAccess(principal, userId); @@ -104,7 +103,7 @@ public String showProfileEditForm(@AuthenticationPrincipal SecurityUser principa // Posting information from user edit form @PostMapping("/profile/edit/{userId}") - public String updateProfile(@AuthenticationPrincipal SecurityUser principal, + public String updateProfile(@AuthenticationPrincipal UserPrincipal principal, @PathVariable Long userId, @RequestParam String fullName, @RequestParam String email, @@ -124,7 +123,7 @@ public String updateProfile(@AuthenticationPrincipal SecurityUser principal, // A list view of users only available to sysadmins @GetMapping("/user/list") - public String userListView(@AuthenticationPrincipal SecurityUser principal, + public String userListView(@AuthenticationPrincipal UserPrincipal principal, Model model) { userService.validateSysAdmin(principal); List allUsers = userService.findAll(); @@ -134,7 +133,7 @@ public String userListView(@AuthenticationPrincipal SecurityUser principal, } @GetMapping("/dashboard/applicant") - public String applicantDashboard(@AuthenticationPrincipal SecurityUser principal, + public String applicantDashboard(@AuthenticationPrincipal UserPrincipal principal, Model model) { List visas = visaService.findVisasByApplicantId(principal.getUserId()); model.addAttribute("name", principal.getFullName()); @@ -143,7 +142,7 @@ public String applicantDashboard(@AuthenticationPrincipal SecurityUser principal } @GetMapping("/dashboard/admin") - public String adminDashboard(@AuthenticationPrincipal SecurityUser principal, + public String adminDashboard(@AuthenticationPrincipal UserPrincipal principal, Model model) { userService.validateAdmin(principal); List assignedCases = visaService.findVisasByHandlerId(principal.getUserId()); @@ -155,7 +154,7 @@ public String adminDashboard(@AuthenticationPrincipal SecurityUser principal, } @GetMapping("/dashboard/sysadmin") - public String sysAdminDashboard(@AuthenticationPrincipal SecurityUser principal, + public String sysAdminDashboard(@AuthenticationPrincipal UserPrincipal principal, Model model) { userService.validateSysAdmin(principal); List allUsers = userService.findAll(); diff --git a/src/main/java/org/example/visacasemanagementsystem/user/entity/User.java b/src/main/java/org/example/visacasemanagementsystem/user/entity/User.java index 829ff50..8aaa9c9 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/entity/User.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/entity/User.java @@ -22,14 +22,18 @@ public class User { @Column(nullable = false, updatable = false) private Long id; + @NotBlank @Column(unique = true, nullable = false) + private String username; + + @NotBlank @Column(nullable = false) + private String password; + @NotBlank @Column(nullable = false) private String fullName; @NotBlank @Column(unique = true) private String email; - //Placeholder password storage solution - @NotBlank private String password; @NotNull @Enumerated(EnumType.STRING) private UserAuthorization userAuthorization; 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 1675c15..881cd9c 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java @@ -28,6 +28,7 @@ public User toEntity(CreateUserDTO dto){ User user = new User(); user.setFullName(dto.fullName()); user.setEmail(dto.email()); + user.setUsername(dto.email()); user.setUserAuthorization(dto.userAuthorization()); return user; } @@ -38,5 +39,6 @@ public void updateEntityFromDTO(UpdateUserDTO dto, User user) { user.setFullName(dto.fullName()); user.setEmail(dto.email()); + user.setUsername(dto.email()); } } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/repository/UserRepository.java b/src/main/java/org/example/visacasemanagementsystem/user/repository/UserRepository.java index 7fbbe1b..84eff5e 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/repository/UserRepository.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/repository/UserRepository.java @@ -3,7 +3,6 @@ import org.example.visacasemanagementsystem.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; - import java.util.Optional; @Repository @@ -11,4 +10,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByUsername(String username); + } diff --git a/src/main/java/org/example/visacasemanagementsystem/user/security/SecurityUser.java b/src/main/java/org/example/visacasemanagementsystem/user/security/SecurityUser.java deleted file mode 100644 index 01c40e6..0000000 --- a/src/main/java/org/example/visacasemanagementsystem/user/security/SecurityUser.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.example.visacasemanagementsystem.user.security; - -import lombok.Getter; -import org.example.visacasemanagementsystem.user.entity.User; -import org.jspecify.annotations.NullMarked; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Collection; -import java.util.List; - -public class SecurityUser implements UserDetails, Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - @Getter - private final Long userId; - - @Getter - private final String fullName; - private final String email; - private final String password; - private final Collection authorities; - - public SecurityUser(User user) { - this.userId = user.getId(); - this.fullName = user.getFullName(); - this.email = user.getEmail(); - this.password = user.getPassword(); - this.authorities = List.of( - new SimpleGrantedAuthority("ROLE_" + user.getUserAuthorization().name()) - ); - } - - @Override - @NullMarked - public Collection getAuthorities() { - return authorities; - } - - @Override - public String getPassword() { - return password; - } - - @Override - @NullMarked - public String getUsername() { - return email; - } -} \ No newline at end of file diff --git a/src/main/java/org/example/visacasemanagementsystem/user/security/UserDetailsServiceImpl.java b/src/main/java/org/example/visacasemanagementsystem/user/security/UserDetailsServiceImpl.java index a1463f7..35c900b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/security/UserDetailsServiceImpl.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/security/UserDetailsServiceImpl.java @@ -19,9 +19,10 @@ public UserDetailsServiceImpl(UserRepository userRepository) { @Override @NullMarked - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("No user found with email: " + email)); - return new SecurityUser(user); + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + + return new UserPrincipal(user); } } \ No newline at end of file diff --git a/src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java b/src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java new file mode 100644 index 0000000..c6f8eb5 --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java @@ -0,0 +1,45 @@ +package org.example.visacasemanagementsystem.user.security; + +import org.example.visacasemanagementsystem.user.entity.User; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.Collections; + +public class UserPrincipal implements UserDetails { + + private final User user; + + public UserPrincipal(User user) { + this.user = user; + } + + @Override + @NullMarked + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority(user.getUserAuthorization().asAuthority())); + } + + public String getFullName() { + return user.getFullName(); + } + + + @Override + public @Nullable String getPassword() { + return user.getPassword(); + } + + @Override + @NullMarked + public String getUsername() { + return user.getUsername(); + } + + public Long getUserId() { + return user.getId(); + } +} \ No newline at end of file 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 69b1114..c85ef87 100644 --- a/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java +++ b/src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java @@ -10,11 +10,11 @@ import org.example.visacasemanagementsystem.user.entity.User; import org.example.visacasemanagementsystem.user.mapper.UserMapper; import org.example.visacasemanagementsystem.user.repository.UserRepository; -import org.example.visacasemanagementsystem.user.security.SecurityUser; +import org.example.visacasemanagementsystem.user.security.UserPrincipal; import org.springframework.dao.DataIntegrityViolationException; +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; @@ -25,10 +25,12 @@ public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; private static final String USER_NOT_FOUND = "User not found"; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository userRepository, UserMapper userMapper) { + public UserService(UserRepository userRepository, UserMapper userMapper, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.userMapper = userMapper; + this.passwordEncoder = passwordEncoder; } public List findAll() { @@ -51,13 +53,13 @@ public Optional findByEmail(String email) { @Transactional public UserDTO createUser(@Valid CreateUserDTO dto) { User user = userMapper.toEntity(dto); - user.setPassword(dto.password()); // mapper doesn't set this currently if (dto.password().length() < 8) { throw new IllegalArgumentException("Password must be at least 8 characters"); } + user.setPassword(passwordEncoder.encode(dto.password())); + user.setUserAuthorization(UserAuthorization.USER); try { User savedUser = userRepository.save(user); - savedUser.setUserAuthorization(UserAuthorization.USER); return userMapper.toDTO(savedUser); } catch (DataIntegrityViolationException e) { throw new IllegalArgumentException("A user with this email already exists", e); @@ -108,7 +110,7 @@ private void validateSysAdmin(Long requesterId) { } } - public void validateProfileAccess(SecurityUser principal, Long userId) { + public void validateProfileAccess(UserPrincipal principal, Long userId) { boolean isOwnProfile = principal.getUserId().equals(userId); boolean isSysAdmin = principal.getAuthorities().stream() .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_SYSADMIN")); @@ -118,7 +120,7 @@ public void validateProfileAccess(SecurityUser principal, Long userId) { } } - public void validateSysAdmin(SecurityUser principal) { + public void validateSysAdmin(UserPrincipal principal) { boolean isSysAdmin = principal.getAuthorities().stream() .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_SYSADMIN")); @@ -127,7 +129,7 @@ public void validateSysAdmin(SecurityUser principal) { } } - public void validateAdmin(SecurityUser principal) { + public void validateAdmin(UserPrincipal principal) { boolean isSysAdmin = principal.getAuthorities().stream() .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_SYSADMIN")); boolean isAdmin = principal.getAuthorities().stream() diff --git a/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java index bb0e272..8bc7662 100644 --- a/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java @@ -45,6 +45,7 @@ void setUp() { testUser = new User(); testUser.setFullName("TestUser"); testUser.setEmail("test@example.com"); + testUser.setUsername("test@example.com"); testUser.setPassword("password123"); testUser.setUserAuthorization(UserAuthorization.USER); testUser = userRepository.save(testUser); diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java index 07f90a4..ad59c6c 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java @@ -106,8 +106,10 @@ void updateVisa_shouldUpdateVisaAndResetStatus_WhenUserIsAuthorized() { void assignHandler_shouldAssignAdminToVisa_AndChangeStatusToAssigned() { // Arrange User admin = new User(); + String adminEmail = "admin@test2.com"; admin.setFullName("Test Admin"); - admin.setEmail("admin@test2.com"); + admin.setEmail(adminEmail); + admin.setUsername(adminEmail); admin.setPassword("password123"); admin.setUserAuthorization(UserAuthorization.ADMIN); admin = userRepository.save(admin); @@ -146,8 +148,10 @@ void assignHandler_shouldAssignAdminToVisa_AndChangeStatusToAssigned() { private User createAndSaveValidUser() { User user = new User(); + String testEmail = java.util.UUID.randomUUID() + "@test.com"; // Unik mail varje gång user.setFullName("Test User"); - user.setEmail(java.util.UUID.randomUUID() + "@test.com"); // Unik mail varje gång + user.setEmail(testEmail); + user.setUsername(testEmail); user.setPassword("password"); user.setUserAuthorization(UserAuthorization.USER); return userRepository.save(user);