diff --git a/pom.xml b/pom.xml index 81738c6..08bc5f5 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,10 @@ org.flywaydb flyway-mysql + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java index 293d5b5..01e2a36 100644 --- a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java +++ b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java @@ -6,6 +6,8 @@ import org.springframework.security.config.annotation.method.configuration.Enabl 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.LogoutConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @@ -13,6 +15,10 @@ import org.springframework.security.web.SecurityFilterChain; @EnableMethodSecurity public class WebSecurityConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(8); + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http diff --git a/src/main/java/net/sytes/kashey/sweater/controller/ControllerUtils.java b/src/main/java/net/sytes/kashey/sweater/controller/ControllerUtils.java new file mode 100644 index 0000000..13d4aa7 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/controller/ControllerUtils.java @@ -0,0 +1,19 @@ +package net.sytes.kashey.sweater.controller; + +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import java.util.Map; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +public class ControllerUtils { + static Map getErrors(BindingResult bindingResult) { + + Collector> collector = Collectors.toMap( + fieldError -> fieldError.getField() + "Error", + FieldError::getDefaultMessage + ); + return bindingResult.getFieldErrors().stream().collect(collector); + } +} \ No newline at end of file diff --git a/src/main/java/net/sytes/kashey/sweater/controller/MainController.java b/src/main/java/net/sytes/kashey/sweater/controller/MainController.java index 7f43371..1dfa3d0 100644 --- a/src/main/java/net/sytes/kashey/sweater/controller/MainController.java +++ b/src/main/java/net/sytes/kashey/sweater/controller/MainController.java @@ -1,5 +1,6 @@ package net.sytes.kashey.sweater.controller; +import jakarta.validation.Valid; import net.sytes.kashey.sweater.domain.Message; import net.sytes.kashey.sweater.repository.MessageRepository; import net.sytes.kashey.sweater.security.CustomUserDetails; @@ -7,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -14,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -48,25 +51,34 @@ public class MainController { @PostMapping("/main") public String addMessage(@AuthenticationPrincipal CustomUserDetails customUserDetails, - @RequestParam String text, - @RequestParam String tag, - @RequestParam("file") MultipartFile file, - Model model) throws IOException { + @Valid Message message, + BindingResult bindingResult, + Model model, + @RequestParam("file") MultipartFile file + ) throws IOException { - Message newMessage = new Message(text, tag, customUserDetails.getUser()); + message.setAuthor(customUserDetails.getUser()); - if (file != null && !Objects.requireNonNull(file.getOriginalFilename()).isEmpty()) { - File uploadDir = new File(uploadPath); // Директория на сервере, куда помещаем пришедший файл - if (!uploadDir.exists()) { - uploadDir.mkdirs(); + if (bindingResult.hasErrors()) { + Map errorsMap = ControllerUtils.getErrors(bindingResult); + model.mergeAttributes(errorsMap); + model.addAttribute("message", message); + } else { + + if (file != null && !Objects.requireNonNull(file.getOriginalFilename()).isEmpty()) { + File uploadDir = new File(uploadPath); // Директория на сервере, куда помещаем пришедший файл + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + String uuidFile = UUID.randomUUID().toString(); + String resultFilename = uuidFile + "." + file.getOriginalFilename(); // случайный уид + имя файла с клиента + file.transferTo(new File(uploadPath + "/" + resultFilename)); // помещаем переименованный файл с клиента в директорию сервера + message.setFilename(resultFilename); } - String uuidFile = UUID.randomUUID().toString(); - String resultFilename = uuidFile + "." + file.getOriginalFilename(); // случайный уид + имя файла с клиента - file.transferTo(new File(uploadPath + "/" + resultFilename)); // помещаем переименованный файл с клиента в директорию сервера - newMessage.setFilename(resultFilename); - } - messageRepository.save(newMessage); + messageRepository.save(message); + model.addAttribute("message", null); + } model.addAttribute("messages", messageRepository.findAll()); model.addAttribute("tag", ""); return "main"; diff --git a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java index 1e21ef1..a7d6424 100644 --- a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java +++ b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java @@ -1,17 +1,16 @@ package net.sytes.kashey.sweater.controller; +import jakarta.validation.Valid; import net.sytes.kashey.sweater.domain.User; -import net.sytes.kashey.sweater.repository.UserRepository; import net.sytes.kashey.sweater.service.CustomUserDetailsService; 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.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import java.util.Map; - @Controller public class RegistrationController { private final CustomUserDetailsService customUserDetailsService; @@ -27,11 +26,20 @@ public class RegistrationController { } @PostMapping("/registration") - public String addUser(@ModelAttribute User user, Map model) { + public String addUser(@ModelAttribute @Valid User user, BindingResult bindingResult, Model model) { + + if (user.getPassword() != null && !user.getPassword().equals(user.getPassword2())) { + model.addAttribute("passwordError", "Passwords do not match!"); + } + + if (bindingResult.hasErrors()) { + model.mergeAttributes(ControllerUtils.getErrors(bindingResult)); + return "registration"; + } String result = customUserDetailsService.addUser(user); if (!result.isEmpty()) { - model.put("message", result); + model.addAttribute("usernameError", result); return "registration"; } else { return "redirect:/login"; diff --git a/src/main/java/net/sytes/kashey/sweater/domain/Message.java b/src/main/java/net/sytes/kashey/sweater/domain/Message.java index 4048587..fd95fae 100644 --- a/src/main/java/net/sytes/kashey/sweater/domain/Message.java +++ b/src/main/java/net/sytes/kashey/sweater/domain/Message.java @@ -1,13 +1,19 @@ package net.sytes.kashey.sweater.domain; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.Length; @Entity public class Message { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; + + @NotBlank(message = "Please fill in the field.") + @Length(max = 2048, message = "Message too long.") private String text; + @Length(max = 255, message = "Tag too long.") private String tag; private String filename; diff --git a/src/main/java/net/sytes/kashey/sweater/domain/User.java b/src/main/java/net/sytes/kashey/sweater/domain/User.java index 86819dd..8af54ff 100644 --- a/src/main/java/net/sytes/kashey/sweater/domain/User.java +++ b/src/main/java/net/sytes/kashey/sweater/domain/User.java @@ -1,6 +1,8 @@ package net.sytes.kashey.sweater.domain; import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import java.util.Set; @@ -11,10 +13,18 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; + @NotBlank(message = "Username must not be empty.") private String username; + @NotBlank(message = "Password must not be empty.") private String password; + + @Transient + @NotBlank(message = "Password confirmation must not be empty.") + private String password2; private boolean enabled; + @Email(message = "E-mail is not valid.") + @NotBlank(message = "E-mail must not be empty.") private String email; private String activationCode; @@ -90,4 +100,12 @@ public class User { public void setActivationCode(String activationCode) { this.activationCode = activationCode; } + + public String getPassword2() { + return password2; + } + + public void setPassword2(String password2) { + this.password2 = password2; + } } diff --git a/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java b/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java index 398662d..c8e02d8 100644 --- a/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java +++ b/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; 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.util.ObjectUtils; @@ -19,19 +20,26 @@ public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; private final EmailService emailService; + private final PasswordEncoder passwordEncoder; @Value("${hosting.path}") private String hostingPath; - public CustomUserDetailsService(UserRepository userRepository, EmailService emailService) { + public CustomUserDetailsService(UserRepository userRepository, EmailService emailService, + PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.emailService = emailService; + this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { + if (name == null) { + throw new UsernameNotFoundException("Username is empty!"); + } + User user = userRepository.findByUsername(name) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + name)); return new CustomUserDetails(user); @@ -50,7 +58,7 @@ public class CustomUserDetailsService implements UserDetailsService { user.setEnabled(true); user.setActivationCode(UUID.randomUUID().toString()); user.setRoles(Collections.singleton(Role.USER)); - user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей + user.setPassword(passwordEncoder.encode(user.getPassword())); userRepository.save(user); sendMessage(user); @@ -114,7 +122,7 @@ public class CustomUserDetailsService implements UserDetailsService { } if (!ObjectUtils.isEmpty(password)) { - user.setPassword("{noop}" + password); + user.setPassword(password); } userRepository.save(user); diff --git a/src/main/resources/db/migration/V2__Add_admin.sql b/src/main/resources/db/migration/V2__Add_admin.sql index a2ac870..f8aafcf 100644 --- a/src/main/resources/db/migration/V2__Add_admin.sql +++ b/src/main/resources/db/migration/V2__Add_admin.sql @@ -1,5 +1,5 @@ insert into usr (id, username, password, enabled) -values (1, 'admin', '{noop}admin', true); +values (1, 'admin', '$2a$12$.XB1cxFOs6F8vgouixcTP.TaY2GXwFspYHID12JpLwotvoYI0JwnC', true); insert into user_role (user_id, roles) values (1, 'USER'), (1, 'ADMIN'); \ No newline at end of file diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index b9fdbc5..fca2ec4 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -2,8 +2,10 @@ <#import "parts/login.ftl" as l> <@com.page> - <#if message??> -
${message}
+ <#if Session?? && Session.SPRING_SECURITY_LAST_EXCEPTION??> + <@l.login "/login" false/> \ No newline at end of file diff --git a/src/main/resources/templates/main.ftl b/src/main/resources/templates/main.ftl index eef5522..10f520e 100644 --- a/src/main/resources/templates/main.ftl +++ b/src/main/resources/templates/main.ftl @@ -12,14 +12,26 @@ aria-controls="collapseExample"> Click to add message -
+
- + + <#if textError??> +
+ ${textError} +
+
- + + <#if tagError??> +
+ ${tagError} +
+
diff --git a/src/main/resources/templates/parts/login.ftl b/src/main/resources/templates/parts/login.ftl index 7a8489d..22d5dec 100644 --- a/src/main/resources/templates/parts/login.ftl +++ b/src/main/resources/templates/parts/login.ftl @@ -3,24 +3,57 @@
- + + <#if usernameError??> +
+ ${usernameError} +
+
- + + <#if passwordError??> +
+ ${passwordError} +
+
<#if isRegistrationForm> +
+ +
+ + <#if password2Error??> +
+ ${password2Error} +
+ +
+
- + + <#if emailError??> +
+ ${emailError} +
+
- <#if !isRegistrationForm> Add new user