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}
+ #if>
<@l.login "/login" false/>
@com.page>
\ 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>
+
+ <#if isRegistrationForm>
+
+ #if>
+
<#if !isRegistrationForm>
Add new user
#if>
- value="Create" <#else> value="Sign in"#if>/>
+ value="Create" <#else> value="Sign in"#if>/>
#macro>
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>