diff --git a/backend/data.mv.db b/backend/data.mv.db index 5a48025..8a8df70 100644 Binary files a/backend/data.mv.db and b/backend/data.mv.db differ diff --git a/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java b/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java new file mode 100644 index 0000000..c6ea06f --- /dev/null +++ b/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java @@ -0,0 +1,63 @@ +package com.example.backend.core.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +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.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +import com.example.backend.core.configurations.Constants; +import com.example.backend.users.api.UserSignupController; +import com.example.backend.users.model.UserRole; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + @Bean + SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin)); + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.cors(Customizer.withDefaults()); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers("/css/**", "/webjars/**", "/*.svg") + .permitAll()); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers(Constants.ADMIN_PREFIX + "/**").hasRole(UserRole.ADMIN.name()) + .requestMatchers("/h2-console/**").hasRole(UserRole.ADMIN.name()) + .requestMatchers(UserSignupController.URL).anonymous() + .requestMatchers(Constants.LOGIN_URL).anonymous() + .anyRequest().authenticated()); + + httpSecurity.formLogin(formLogin -> formLogin + .loginPage(Constants.LOGIN_URL)); + + httpSecurity.rememberMe(rememberMe -> rememberMe.key("uniqueAndSecret")); + + httpSecurity.logout(logout -> logout + .deleteCookies("JSESSIONID")); + + return httpSecurity.build(); + } + + @Bean + DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { + final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java b/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java new file mode 100644 index 0000000..d51cf25 --- /dev/null +++ b/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java @@ -0,0 +1,64 @@ +package com.example.backend.core.security; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.example.backend.users.model.UserEntity; + +public class UserPrincipal implements UserDetails { + private final long id; + private final String username; + private final String password; + private final Set roles; + private final boolean active; + + public UserPrincipal(UserEntity user) { + this.id = user.getId(); + this.username = user.getLogin(); + this.password = user.getPassword(); + this.roles = Set.of(user.getRole()); + this.active = true; + } + + public Long getId() { + return id; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public Collection getAuthorities() { + return roles; + } + + @Override + public boolean isEnabled() { + return active; + } + + @Override + public boolean isAccountNonExpired() { + return isEnabled(); + } + + @Override + public boolean isAccountNonLocked() { + return isEnabled(); + } + + @Override + public boolean isCredentialsNonExpired() { + return isEnabled(); + } +} diff --git a/backend/src/main/java/com/example/backend/users/api/UserSignupController.java b/backend/src/main/java/com/example/backend/users/api/UserSignupController.java new file mode 100644 index 0000000..72a0d56 --- /dev/null +++ b/backend/src/main/java/com/example/backend/users/api/UserSignupController.java @@ -0,0 +1,65 @@ +package com.example.backend.users.api; + +import java.util.Objects; + +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.example.backend.core.configurations.Constants; +import com.example.backend.users.model.UserEntity; +import com.example.backend.users.service.UserService; + +import jakarta.validation.Valid; + +@Controller +@RequestMapping(UserSignupController.URL) +public class UserSignupController { + public static final String URL = "/signup"; + + private static final String SIGNUP_VIEW = "signup"; + private static final String USER_ATTRIBUTE = "user"; + + private final UserService userService; + private final ModelMapper modelMapper; + + public UserSignupController( + UserService userService, + ModelMapper modelMapper) { + this.userService = userService; + this.modelMapper = modelMapper; + } + + private UserEntity toEntity(UserSignupDTO dto) { + return modelMapper.map(dto, UserEntity.class); + } + + @GetMapping + public String getSignup(Model model) { + model.addAttribute(USER_ATTRIBUTE, new UserSignupDTO()); + return SIGNUP_VIEW; + } + + @PostMapping + public String signup( + @ModelAttribute(name = USER_ATTRIBUTE) @Valid UserSignupDTO user, + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + return SIGNUP_VIEW; + } + if (!Objects.equals(user.getPassword(), user.getPasswordConfirm())) { + bindingResult.rejectValue("password", "signup:passwords", "Пароли не совпадают."); + model.addAttribute(USER_ATTRIBUTE, user); + return SIGNUP_VIEW; + } + userService.create(toEntity(user)); + return Constants.REDIRECT_VIEW + Constants.LOGIN_URL + "?signup"; + } + +} diff --git a/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java b/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java new file mode 100644 index 0000000..3cdb770 --- /dev/null +++ b/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java @@ -0,0 +1,40 @@ +package com.example.backend.users.api; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class UserSignupDTO { + @NotBlank + @Size(min = 3, max = 20) + private String login; + @NotBlank + @Size(min = 3, max = 20) + private String password; + @NotBlank + @Size(min = 3, max = 20) + private String passwordConfirm; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public void setPasswordConfirm(String passwordConfirm) { + this.passwordConfirm = passwordConfirm; + } +} diff --git a/backend/src/main/java/com/example/backend/users/model/UserEntity.java b/backend/src/main/java/com/example/backend/users/model/UserEntity.java index 54c7a0e..f43a973 100644 --- a/backend/src/main/java/com/example/backend/users/model/UserEntity.java +++ b/backend/src/main/java/com/example/backend/users/model/UserEntity.java @@ -13,7 +13,7 @@ import jakarta.persistence.Table; public class UserEntity extends BaseEntity { @Column(nullable = false, unique = true, length = 15) - private String username; + private String login; @Column(nullable = false, length = 5) private String password; @@ -24,18 +24,18 @@ public class UserEntity extends BaseEntity { } - public UserEntity(Integer id, String username, String password) { - this.username = username; + public UserEntity(Integer id, String login, String password) { + this.login = login; this.password = password; this.role = UserRole.USER; } - public String getUsername() { - return username; + public String getLogin() { + return login; } - public void setUsername(String username) { - this.username = username; + public void setLogin(String login) { + this.login = login; } public String getPassword() { @@ -56,7 +56,7 @@ public class UserEntity extends BaseEntity { @Override public int hashCode() { - return Objects.hash(id, username, password, role); + return Objects.hash(id, login, password, role); } @Override @@ -67,7 +67,7 @@ public class UserEntity extends BaseEntity { return false; final UserEntity other = (UserEntity) obj; return Objects.equals(other.getId(), id) && - Objects.equals(other.getUsername(), username) && + Objects.equals(other.getLogin(), login) && Objects.equals(other.getRole(), role) && Objects.equals(other.getPassword(), password); } diff --git a/backend/src/main/java/com/example/backend/users/repository/UserRepository.java b/backend/src/main/java/com/example/backend/users/repository/UserRepository.java index f873c56..0ecae0e 100644 --- a/backend/src/main/java/com/example/backend/users/repository/UserRepository.java +++ b/backend/src/main/java/com/example/backend/users/repository/UserRepository.java @@ -7,4 +7,6 @@ import com.example.backend.users.model.UserEntity; public interface UserRepository extends CrudRepository { Optional findByUsernameIgnoreCase(String username); + + Optional findByLoginIgnoreCase(String login); } diff --git a/backend/src/main/java/com/example/backend/users/service/UserService.java b/backend/src/main/java/com/example/backend/users/service/UserService.java index ed5d226..b43472c 100644 --- a/backend/src/main/java/com/example/backend/users/service/UserService.java +++ b/backend/src/main/java/com/example/backend/users/service/UserService.java @@ -1,22 +1,41 @@ package com.example.backend.users.service; import java.util.List; +import java.util.Optional; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.util.stream.StreamSupport; +import com.example.backend.core.configurations.Constants; import com.example.backend.core.errors.NotFoundException; +import com.example.backend.core.security.UserPrincipal; import com.example.backend.users.model.UserEntity; +import com.example.backend.users.model.UserRole; import com.example.backend.users.repository.UserRepository; @Service -public class UserService { +public class UserService implements UserDetailsService { private final UserRepository repository; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository repository) { + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; + this.passwordEncoder = passwordEncoder; + } + + private void checkLogin(Integer id, String login) { + final Optional existsUser = repository.findByLoginIgnoreCase(login); + if (existsUser.isPresent() && !existsUser.get().getId().equals(id)) { + throw new IllegalArgumentException( + String.format("User with login %s is already exists", login)); + } } @Transactional(readOnly = true) @@ -29,18 +48,35 @@ public class UserService { return repository.findById(id).orElseThrow(() -> new NotFoundException(id)); } + @Transactional(readOnly = true) + private UserEntity getByLogin(String username) { + return repository.findByLoginIgnoreCase(username) + .orElseThrow(() -> new IllegalArgumentException("Такого логина нет... хде вы его взяли?")); + } + @Transactional public UserEntity create(UserEntity entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity is null"); + } + checkLogin(null, entity.getLogin()); + final String password = Optional.ofNullable(entity.getPassword()).orElse(""); + entity.setPassword( + passwordEncoder.encode(StringUtils.hasText(password.strip()) ? password : Constants.DEFAULT_PASSWORD)); + entity.setRole(Optional.ofNullable(entity.getRole()).orElse(UserRole.USER)); + repository.save(entity); return repository.save(entity); } @Transactional public UserEntity update(Integer id, UserEntity entity) { final UserEntity existsentity = get(id); - existsentity.setUsername(entity.getUsername()); + checkLogin(id, entity.getLogin()); + existsentity.setLogin(entity.getLogin()); existsentity.setPassword(entity.getPassword()); - existsentity.setIsAdmin(entity.getIsAdmin()); - return repository.save(existsentity); + existsentity.setRole(entity.getRole()); + repository.save(existsentity); + return existsentity; } @Transactional @@ -50,4 +86,10 @@ public class UserService { return existsentity; } + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final UserEntity existUser = getByLogin(username); + return new UserPrincipal(existUser); + } }