From 2736f3ddf64c98e812f24534db092a7da2c81018 Mon Sep 17 00:00:00 2001 From: Leonid Date: Sun, 16 Feb 2025 00:19:45 +0300 Subject: [PATCH] =?UTF-8?q?-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=BA=D0=BA=D0=B0?= =?UTF-8?q?=D1=83=D0=BD=D1=82=D0=B0=20=D0=BF=D0=BE=20=D1=8D=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D1=80=D0=BE=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=87=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://www.youtube.com/watch?v=yBXs_gtSmUc&list=PLU2ftbIeotGpAYRP9Iv2KLIwK36-o_qYk&index=12 --- pom.xml | 4 + .../sweater/config/WebSecurityConfig.java | 2 +- .../controller/RegistrationController.java | 40 ++++--- .../net/sytes/kashey/sweater/domain/User.java | 20 ++++ .../sweater/repository/UserRepository.java | 2 + .../service/CustomUserDetailsService.java | 57 +++++++++- .../kashey/sweater/service/EmailService.java | 31 +++++ src/main/resources/application.properties | 8 +- src/main/resources/static/custom.css | 106 ++++++++++++++++++ src/main/resources/static/style.css | 3 - src/main/resources/templates/login.ftl | 3 + src/main/resources/templates/main.ftl | 1 + src/main/resources/templates/parts/common.ftl | 1 - src/main/resources/templates/parts/login.ftl | 14 ++- src/main/resources/templates/parts/navbar.ftl | 1 + src/main/resources/templates/registration.ftl | 2 +- src/main/resources/templates/userList.ftl | 4 +- 17 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 src/main/java/net/sytes/kashey/sweater/service/EmailService.java create mode 100644 src/main/resources/static/custom.css delete mode 100644 src/main/resources/static/style.css diff --git a/pom.xml b/pom.xml index 5a0d73b..46299bd 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,10 @@ spring-security-core 6.3.1 + + org.springframework.boot + spring-boot-starter-mail + 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 1ab2de3..293d5b5 100644 --- a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java +++ b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java @@ -17,7 +17,7 @@ public class WebSecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((requests) -> requests - .requestMatchers("/", "/registration","/static/**").permitAll() + .requestMatchers("/", "/registration","/static/**", "/activate/*").permitAll() .anyRequest().authenticated() ) .formLogin((form) -> form 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 7c2e4fd..1e21ef1 100644 --- a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java +++ b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java @@ -1,23 +1,23 @@ package net.sytes.kashey.sweater.controller; -import net.sytes.kashey.sweater.domain.Role; 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.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.Collections; import java.util.Map; @Controller public class RegistrationController { + private final CustomUserDetailsService customUserDetailsService; - private final UserRepository userRepository; - - public RegistrationController(UserRepository userRepository) { - this.userRepository = userRepository; + public RegistrationController(CustomUserDetailsService customUserDetailsService) { + this.customUserDetailsService = customUserDetailsService; } @GetMapping("/registration") @@ -29,20 +29,24 @@ public class RegistrationController { @PostMapping("/registration") public String addUser(@ModelAttribute User user, Map model) { - if (user.getUsername() == null || user.getUsername().isEmpty() || user.getUsername().isBlank()) { - model.put("message", "User name must not be empty"); + String result = customUserDetailsService.addUser(user); + if (!result.isEmpty()) { + model.put("message", result); return "registration"; + } else { + return "redirect:/login"; + } + } + + @GetMapping("/activate/{activateCode}") + public String activateAccount(Model model, @PathVariable String activateCode) { + + if (customUserDetailsService.activateUser(activateCode)) { + model.addAttribute("message", "User successfully activated!"); + } else { + model.addAttribute("message", "User activation is failed!"); } - if (userRepository.findByUsername(user.getUsername()).isPresent()) { - model.put("message", "User already exists"); - return "registration"; - } - - user.setEnabled(true); - user.setRoles(Collections.singleton(Role.USER)); - user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей - userRepository.save(user); - return "redirect:/login"; + return "login"; } } 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 368517d..86819dd 100644 --- a/src/main/java/net/sytes/kashey/sweater/domain/User.java +++ b/src/main/java/net/sytes/kashey/sweater/domain/User.java @@ -14,6 +14,10 @@ public class User { private String username; private String password; private boolean enabled; + + private String email; + + private String activationCode; @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER) @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id")) @Enumerated(EnumType.STRING) @@ -70,4 +74,20 @@ public class User { public void setRoles(Set roles) { this.roles = roles; } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getActivationCode() { + return activationCode; + } + + public void setActivationCode(String activationCode) { + this.activationCode = activationCode; + } } diff --git a/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java b/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java index 44e2231..06e1394 100644 --- a/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java +++ b/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java @@ -9,4 +9,6 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByUsername(String name); + + User findByActivationCode(String activationCode); } 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 4fcf513..7d5cff3 100644 --- a/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java +++ b/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java @@ -1,21 +1,32 @@ package net.sytes.kashey.sweater.service; +import net.sytes.kashey.sweater.domain.Role; import net.sytes.kashey.sweater.domain.User; import net.sytes.kashey.sweater.repository.UserRepository; import net.sytes.kashey.sweater.security.CustomUserDetails; +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.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.util.Collections; +import java.util.UUID; @Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; + private final EmailService emailService; - public CustomUserDetailsService(UserRepository userRepository) { + @Value("${hosting.path}") + private String hostingPath; + + public CustomUserDetailsService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; + this.emailService = emailService; } @Override @@ -25,4 +36,48 @@ public class CustomUserDetailsService implements UserDetailsService { .orElseThrow(() -> new UsernameNotFoundException("User not found: " + name)); return new CustomUserDetails(user); } + + public String addUser(User user) { + + if (user.getUsername() == null || user.getUsername().isEmpty() || user.getUsername().isBlank()) { + return "User name must not be empty!"; + } + + if (userRepository.findByUsername(user.getUsername()).isPresent()) { + return "User already exists!"; + } + + user.setEnabled(true); + user.setActivationCode(UUID.randomUUID().toString()); + user.setRoles(Collections.singleton(Role.USER)); + user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей + userRepository.save(user); + + if (!ObjectUtils.isEmpty(user.getEmail())) { + + String message = String.format( + "Hello, %s! \n" + + "Welcome to Sweater. Please, visit next link: %s/activate/%s", + user.getUsername(), + hostingPath, + user.getActivationCode() + ); + + emailService.send(user.getEmail(), "Activation code", message); + } + return ""; + } + + public boolean activateUser(String activationCode) { + + User user = userRepository.findByActivationCode(activationCode); + + if (user == null) { + return false; + } + + user.setActivationCode(null); + userRepository.save(user); + return true; + } } diff --git a/src/main/java/net/sytes/kashey/sweater/service/EmailService.java b/src/main/java/net/sytes/kashey/sweater/service/EmailService.java new file mode 100644 index 0000000..637205d --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/service/EmailService.java @@ -0,0 +1,31 @@ +package net.sytes.kashey.sweater.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class EmailService { + private final JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String username; + + @Autowired + public EmailService(JavaMailSender javaMailSender) { + this.javaMailSender = javaMailSender; + } + + public void send(String mailTo, String subject, String text) { + + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(username); + mailMessage.setTo(mailTo); + mailMessage.setSubject(subject); + mailMessage.setText(text); + + javaMailSender.send(mailMessage); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2a17057..3050627 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,4 +7,10 @@ spring.freemarker.expose-request-attributes=true spring.freemarker.expose-session-attributes=true spring.freemarker.suffix=.ftl upload.path=/home/kashey/soft/sweater/uploads - +hosting.path=http://localhost:8080 +spring.mail.host=smtp.yandex.ru +spring.mail.username=username@yandex.ru +spring.mail.password=pa$$word +spring.mail.port=465 +spring.mail.protocol=smtps +mail.debug=true \ No newline at end of file diff --git a/src/main/resources/static/custom.css b/src/main/resources/static/custom.css new file mode 100644 index 0000000..cce1dce --- /dev/null +++ b/src/main/resources/static/custom.css @@ -0,0 +1,106 @@ + +:root { + --yellow-1c: #ffe100; /* Основной желтый */ + --light-yellow-1c: #FDF5C9; /* Светло-желтый фон */ + --dark-1c: #333333; /* Темный цвет для текста */ + --light-yellow-header-1c: #fbed9e; + --light-grey-1c:#f2f2f2; + --brand-red-1c: #B22222; + --begie: #F5F5DC; + --light-y:#F5F5F5; + --header: #F5DEB3; + --brown-1c: #663300; + --gray-1c: #CCCCCC; +} + +/*!* Фон приложения *!*/ +body { + background-color: var(--light-y) !important; + color: var(--dark-1c); +} + +/* Навигационная панель */ +.navbar { + background-color: var(--gray-1c) !important; + color: var(--dark-1c) !important; +} + +/*.navbar .nav-link {*/ +/* color: var(--brown-1c) !important;*/ +/*}*/ + +/*.navbar .nav-link:hover {*/ +/* color: black !important;*/ +/*}*/ + +/* Кнопки */ +/*.btn-primary {*/ +/* background-color: var(--yellow-1c) !important;*/ +/* border-color: var(--yellow-1c) !important;*/ +/* color: var(--dark-1c) !important;*/ +/*}*/ + +/*.btn-primary:hover {*/ +/* background-color: #E6C200 !important; !* Чуть темнее *!*/ +/* border-color: #E6C200 !important;*/ +/*}*/ + +/*!* Карточки и панели *!*/ +/*.card, .panel {*/ +/* background-color: var(--white-1c) !important;*/ +/* border: 1px solid var(--gray-1c) !important;*/ +/*}*/ + +/*!* Таблицы *!*/ +/*.table {*/ +/* background-color: var(--white-1c);*/ +/* color: var(--dark-1c);*/ +/*}*/ + +/*.table thead th {*/ +/* background-color: var(--yellow-1c);*/ +/* color: var(--dark-1c);*/ +/*}*/ + +/*.table-striped tbody tr:nth-of-type(odd) {*/ +/* background-color: var(--gray-1c);*/ +/*}*/ + +/*!* Поля ввода *!*/ +/*.form-control {*/ +/* background-color: var(--white-1c);*/ +/* border: 1px solid var(--gray-1c);*/ +/* color: var(--dark-1c);*/ +/*}*/ + +/*.form-control:focus {*/ +/* border-color: var(--yellow-1c);*/ +/* box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.5);*/ +/*}*/ + +/*!* Выделение чекбоксов *!*/ +/*.form-check-input:checked {*/ +/* background-color: var(--yellow-1c);*/ +/* border-color: var(--yellow-1c);*/ +/*}*/ + +/*!* Бейджи *!*/ +/*.badge-primary {*/ +/* background-color: var(--yellow-1c);*/ +/* color: var(--dark-1c);*/ +/*}*/ + +/*!* Тени для элементов *!*/ +/*.box-shadow {*/ +/* box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);*/ +/*}*/ + +/*!* Стили для названия бренда (Sweater) *!*/ +/*.navbar-brand {*/ +/* color: var(--brown-1c) !important; !* Темный цвет текста *!*/ +/* font-weight: bold; !* Сделаем жирным *!*/ +/*}*/ + +/*.navbar-brand:hover {*/ +/* color: black !important; !* Черный при наведении *!*/ +/*}*/ \ No newline at end of file diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css deleted file mode 100644 index 10fdddc..0000000 --- a/src/main/resources/static/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - color: #191970; -} \ No newline at end of file diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 8d8525d..b9fdbc5 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -2,5 +2,8 @@ <#import "parts/login.ftl" as l> <@com.page> + <#if message??> +
${message}
+ <@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 a8d1478..eef5522 100644 --- a/src/main/resources/templates/main.ftl +++ b/src/main/resources/templates/main.ftl @@ -1,5 +1,6 @@ <#import "parts/common.ftl" as com> <@com.page> +
diff --git a/src/main/resources/templates/parts/common.ftl b/src/main/resources/templates/parts/common.ftl index 17f81c9..5db2558 100644 --- a/src/main/resources/templates/parts/common.ftl +++ b/src/main/resources/templates/parts/common.ftl @@ -4,7 +4,6 @@ Sweater - diff --git a/src/main/resources/templates/parts/login.ftl b/src/main/resources/templates/parts/login.ftl index bc56ad1..7a8489d 100644 --- a/src/main/resources/templates/parts/login.ftl +++ b/src/main/resources/templates/parts/login.ftl @@ -1,7 +1,7 @@ <#macro login path isRegistrationForm=false>
- +
@@ -12,10 +12,20 @@
+ <#if isRegistrationForm> +
+ +
+ +
+
+ + <#if !isRegistrationForm> Add new user - value="Create" <#else> value="Sign in"/> + value="Create" <#else> value="Sign in"/> diff --git a/src/main/resources/templates/parts/navbar.ftl b/src/main/resources/templates/parts/navbar.ftl index fdd7c03..22cd2df 100644 --- a/src/main/resources/templates/parts/navbar.ftl +++ b/src/main/resources/templates/parts/navbar.ftl @@ -2,6 +2,7 @@ <#import "login.ftl" as l>