diff --git a/pom.xml b/pom.xml
index 8986fb2..2e6a7ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,10 +18,28 @@
1.4.2.Final
+
+ org.passay
+ passay
+ 1.6.0
+
+
+ com.iamnbty.training
+ common
+ 0.0.1-SNAPSHOT
+
org.springframework.boot
spring-boot-starter-actuator
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springframework.kafka
+ spring-kafka-test
+
org.springframework.boot
spring-boot-starter-web
diff --git a/src/main/java/com/iamnbty/training/backend/api/UserApi.java b/src/main/java/com/iamnbty/training/backend/api/UserApi.java
index 492203f..da92a5d 100644
--- a/src/main/java/com/iamnbty/training/backend/api/UserApi.java
+++ b/src/main/java/com/iamnbty/training/backend/api/UserApi.java
@@ -2,10 +2,8 @@
import com.iamnbty.training.backend.business.UserBusiness;
import com.iamnbty.training.backend.exception.BaseException;
-import com.iamnbty.training.backend.model.MLoginRequest;
-import com.iamnbty.training.backend.model.MLoginResponse;
-import com.iamnbty.training.backend.model.MRegisterRequest;
-import com.iamnbty.training.backend.model.MRegisterResponse;
+import com.iamnbty.training.backend.model.*;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -32,6 +30,19 @@ public ResponseEntity register(@RequestBody MRegisterRequest
return ResponseEntity.ok(response);
}
+ @PostMapping("/activate")
+ public ResponseEntity activate(@RequestBody MActivateRequest request) throws BaseException {
+ MActivateResponse response = business.activate(request);
+ return ResponseEntity.ok(response);
+ }
+
+ @PostMapping("/resend-activation-email")
+ public ResponseEntity resendActivationEmail(@RequestBody MResendActivationEmailRequest request) throws BaseException {
+ business.resendActivationEmail(request);
+ return ResponseEntity.status(HttpStatus.OK).build();
+ }
+
+
@GetMapping("/refresh-token")
public ResponseEntity refreshToken() throws BaseException {
String response = business.refreshToken();
diff --git a/src/main/java/com/iamnbty/training/backend/business/EmailBusiness.java b/src/main/java/com/iamnbty/training/backend/business/EmailBusiness.java
index e183c20..b71c2ab 100644
--- a/src/main/java/com/iamnbty/training/backend/business/EmailBusiness.java
+++ b/src/main/java/com/iamnbty/training/backend/business/EmailBusiness.java
@@ -2,22 +2,28 @@
import com.iamnbty.training.backend.exception.BaseException;
import com.iamnbty.training.backend.exception.EmailException;
-import com.iamnbty.training.backend.service.EmailService;
+import com.iamnbty.training.common.EmailRequest;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.ListenableFutureCallback;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
@Service
+@Log4j2
public class EmailBusiness {
- private final EmailService emailService;
+ private final KafkaTemplate kafkaEmailTemplate;
- public EmailBusiness(EmailService emailService) {
- this.emailService = emailService;
+ public EmailBusiness(KafkaTemplate kafkaEmailTemplate) {
+ this.kafkaEmailTemplate = kafkaEmailTemplate;
}
public void sendActivateUserEmail(String email, String name, String token) throws BaseException {
@@ -29,15 +35,31 @@ public void sendActivateUserEmail(String email, String name, String token) throw
throw EmailException.templateNotFound();
}
+ log.info("Token = " + token);
+
String finalLink = "http://localhost:4200/activate/" + token;
html = html.replace("${P_NAME}", name);
- html = html.replace("${LINK}", finalLink);
+ html = html.replace("${P_LINK}", finalLink);
- // prepare subject
- String subject = "Please activate your account";
+ EmailRequest request = new EmailRequest();
+ request.setTo(email);
+ request.setSubject("Please activate your account");
+ request.setContent(html);
+ ListenableFuture> future = kafkaEmailTemplate.send("activation-email", request);
+ future.addCallback(new ListenableFutureCallback<>() {
+ @Override
+ public void onFailure(Throwable throwable) {
+ log.error("Kafka send fail");
+ log.error(throwable);
+ }
- emailService.send(email, subject, html);
+ @Override
+ public void onSuccess(SendResult result) {
+ log.info("Kafka send success");
+ log.info(result);
+ }
+ });
}
private String readEmailTemplate(String filename) throws IOException {
diff --git a/src/main/java/com/iamnbty/training/backend/business/UserBusiness.java b/src/main/java/com/iamnbty/training/backend/business/UserBusiness.java
index 45a32d9..0ef4d29 100644
--- a/src/main/java/com/iamnbty/training/backend/business/UserBusiness.java
+++ b/src/main/java/com/iamnbty/training/backend/business/UserBusiness.java
@@ -5,22 +5,20 @@
import com.iamnbty.training.backend.exception.FileException;
import com.iamnbty.training.backend.exception.UserException;
import com.iamnbty.training.backend.mapper.UserMapper;
-import com.iamnbty.training.backend.model.MLoginRequest;
-import com.iamnbty.training.backend.model.MLoginResponse;
-import com.iamnbty.training.backend.model.MRegisterRequest;
-import com.iamnbty.training.backend.model.MRegisterResponse;
+import com.iamnbty.training.backend.model.*;
import com.iamnbty.training.backend.service.TokenService;
import com.iamnbty.training.backend.service.UserService;
import com.iamnbty.training.backend.util.SecurityUtil;
+import io.netty.util.internal.StringUtil;
+import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
@Service
+@Log4j2
public class UserBusiness {
private final UserService userService;
@@ -29,10 +27,13 @@ public class UserBusiness {
private final UserMapper userMapper;
- public UserBusiness(UserService userService, TokenService tokenService, UserMapper userMapper) {
+ private final EmailBusiness emailBusiness;
+
+ public UserBusiness(UserService userService, TokenService tokenService, UserMapper userMapper, EmailBusiness emailBusiness) {
this.userService = userService;
this.tokenService = tokenService;
this.userMapper = userMapper;
+ this.emailBusiness = emailBusiness;
}
public MLoginResponse login(MLoginRequest request) throws BaseException {
@@ -45,10 +46,17 @@ public MLoginResponse login(MLoginRequest request) throws BaseException {
}
User user = opt.get();
+
+ // verify password
if (!userService.matchPassword(request.getPassword(), user.getPassword())) {
throw UserException.loginFailPasswordIncorrect();
}
+ // verify activate status
+ if (!user.isActivated()) {
+ throw UserException.loginFailUserUnactivated();
+ }
+
MLoginResponse response = new MLoginResponse();
response.setToken(tokenService.tokenize(user));
return response;
@@ -72,11 +80,85 @@ public String refreshToken() throws BaseException {
}
public MRegisterResponse register(MRegisterRequest request) throws BaseException {
- User user = userService.create(request.getEmail(), request.getPassword(), request.getName());
+ String token = SecurityUtil.generateToken();
+ User user = userService.create(request.getEmail(), request.getPassword(), request.getName(), token, nextXMinute(30));
+
+ sendEmail(user);
return userMapper.toRegisterResponse(user);
}
+ public MActivateResponse activate(MActivateRequest request) throws BaseException {
+ String token = request.getToken();
+ if (StringUtil.isNullOrEmpty(token)) {
+ throw UserException.activateNoToken();
+ }
+
+ Optional opt = userService.findByToken(token);
+ if (opt.isEmpty()) {
+ throw UserException.activateFail();
+ }
+
+ User user = opt.get();
+
+ if (user.isActivated()) {
+ throw UserException.activateAlready();
+ }
+
+ Date now = new Date();
+ Date expireDate = user.getTokenExpire();
+ if (now.after(expireDate)) {
+ throw UserException.activateTokenExpire();
+ }
+
+ user.setActivated(true);
+ userService.update(user);
+
+ MActivateResponse response = new MActivateResponse();
+ response.setSuccess(true);
+ return response;
+ }
+
+ public void resendActivationEmail(MResendActivationEmailRequest request) throws BaseException {
+ String email = request.getEmail();
+ if (StringUtil.isNullOrEmpty(email)) {
+ throw UserException.resendActivationEmailNoEmail();
+ }
+
+ Optional opt = userService.findByEmail(email);
+ if (opt.isEmpty()) {
+ throw UserException.resendActivationEmailNotFound();
+ }
+
+ User user = opt.get();
+
+ if (user.isActivated()) {
+ throw UserException.activateAlready();
+ }
+
+ user.setToken(SecurityUtil.generateToken());
+ user.setTokenExpire(nextXMinute(30));
+ user = userService.update(user);
+
+ sendEmail(user);
+ }
+
+ private Date nextXMinute(int minute) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.MINUTE, minute);
+ return calendar.getTime();
+ }
+
+ private void sendEmail(User user) {
+ String token = user.getToken();
+
+ try {
+ emailBusiness.sendActivateUserEmail(user.getEmail(), user.getName(), token);
+ } catch (BaseException e) {
+ e.printStackTrace();
+ }
+ }
+
public String uploadProfilePicture(MultipartFile file) throws BaseException {
// validate file
if (file == null) {
diff --git a/src/main/java/com/iamnbty/training/backend/config/KafkaConfig.java b/src/main/java/com/iamnbty/training/backend/config/KafkaConfig.java
new file mode 100644
index 0000000..4e68355
--- /dev/null
+++ b/src/main/java/com/iamnbty/training/backend/config/KafkaConfig.java
@@ -0,0 +1,39 @@
+package com.iamnbty.training.backend.config;
+
+import com.iamnbty.training.common.EmailRequest;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class KafkaConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String server;
+
+ @Bean
+ public Map producerConfigs() {
+ Map map = new HashMap<>();
+
+ map.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, server);
+ map.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ map.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+
+ return map;
+ }
+
+ @Bean
+ public KafkaTemplate kafkaEmailTemplate() {
+ DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>(producerConfigs());
+ return new KafkaTemplate<>(factory);
+ }
+
+}
diff --git a/src/main/java/com/iamnbty/training/backend/config/SecurityConfig.java b/src/main/java/com/iamnbty/training/backend/config/SecurityConfig.java
index 965d9c8..24c650a 100644
--- a/src/main/java/com/iamnbty/training/backend/config/SecurityConfig.java
+++ b/src/main/java/com/iamnbty/training/backend/config/SecurityConfig.java
@@ -23,6 +23,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
"/actuator/**",
"/user/register",
"/user/login",
+ "/user/activate",
+ "/user/resend-activation-email",
"/socket/**"
};
diff --git a/src/main/java/com/iamnbty/training/backend/entity/User.java b/src/main/java/com/iamnbty/training/backend/entity/User.java
index 1564fdc..6351886 100644
--- a/src/main/java/com/iamnbty/training/backend/entity/User.java
+++ b/src/main/java/com/iamnbty/training/backend/entity/User.java
@@ -4,6 +4,7 @@
import lombok.EqualsAndHashCode;
import javax.persistence.*;
+import java.util.Date;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@@ -28,4 +29,10 @@ public class User extends BaseEntity {
@OneToMany(mappedBy = "user", orphanRemoval = true, fetch = FetchType.EAGER)
private List addresses;
+ private String token;
+
+ private Date tokenExpire;
+
+ private boolean activated;
+
}
diff --git a/src/main/java/com/iamnbty/training/backend/exception/UserException.java b/src/main/java/com/iamnbty/training/backend/exception/UserException.java
index adfa912..13628b4 100644
--- a/src/main/java/com/iamnbty/training/backend/exception/UserException.java
+++ b/src/main/java/com/iamnbty/training/backend/exception/UserException.java
@@ -50,4 +50,36 @@ public static UserException loginFailPasswordIncorrect() {
return new UserException("login.fail");
}
+ public static UserException loginFailUserUnactivated() {
+ return new UserException("login.fail.unactivated");
+ }
+
+ // ACTIVATE
+
+ public static UserException activateNoToken() {
+ return new UserException("activate.no.token");
+ }
+
+ public static UserException activateAlready() {
+ return new UserException("activate.already");
+ }
+
+ public static UserException activateFail() {
+ return new UserException("activate.fail");
+ }
+
+ public static UserException activateTokenExpire() {
+ return new UserException("activate.token.expire");
+ }
+
+ // RESEND ACTIVATION EMAIL
+
+ public static UserException resendActivationEmailNoEmail() {
+ return new UserException("resend.activation.no.email");
+ }
+
+ public static UserException resendActivationEmailNotFound() {
+ return new UserException("resend.activation.fail");
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/com/iamnbty/training/backend/model/MActivateRequest.java b/src/main/java/com/iamnbty/training/backend/model/MActivateRequest.java
new file mode 100644
index 0000000..e996ec3
--- /dev/null
+++ b/src/main/java/com/iamnbty/training/backend/model/MActivateRequest.java
@@ -0,0 +1,10 @@
+package com.iamnbty.training.backend.model;
+
+import lombok.Data;
+
+@Data
+public class MActivateRequest {
+
+ private String token;
+
+}
diff --git a/src/main/java/com/iamnbty/training/backend/model/MActivateResponse.java b/src/main/java/com/iamnbty/training/backend/model/MActivateResponse.java
new file mode 100644
index 0000000..3f3ae5a
--- /dev/null
+++ b/src/main/java/com/iamnbty/training/backend/model/MActivateResponse.java
@@ -0,0 +1,10 @@
+package com.iamnbty.training.backend.model;
+
+import lombok.Data;
+
+@Data
+public class MActivateResponse {
+
+ private boolean success;
+
+}
diff --git a/src/main/java/com/iamnbty/training/backend/model/MResendActivationEmailRequest.java b/src/main/java/com/iamnbty/training/backend/model/MResendActivationEmailRequest.java
new file mode 100644
index 0000000..7c6cd3a
--- /dev/null
+++ b/src/main/java/com/iamnbty/training/backend/model/MResendActivationEmailRequest.java
@@ -0,0 +1,10 @@
+package com.iamnbty.training.backend.model;
+
+import lombok.Data;
+
+@Data
+public class MResendActivationEmailRequest {
+
+ private String email;
+
+}
diff --git a/src/main/java/com/iamnbty/training/backend/repository/UserRepository.java b/src/main/java/com/iamnbty/training/backend/repository/UserRepository.java
index 7e5178c..1a222e6 100644
--- a/src/main/java/com/iamnbty/training/backend/repository/UserRepository.java
+++ b/src/main/java/com/iamnbty/training/backend/repository/UserRepository.java
@@ -9,6 +9,8 @@ public interface UserRepository extends CrudRepository {
Optional findByEmail(String email);
+ Optional findByToken(String token);
+
boolean existsByEmail(String email);
}
diff --git a/src/main/java/com/iamnbty/training/backend/service/EmailService.java b/src/main/java/com/iamnbty/training/backend/service/EmailService.java
deleted file mode 100644
index ee6cf54..0000000
--- a/src/main/java/com/iamnbty/training/backend/service/EmailService.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.iamnbty.training.backend.service;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.mail.javamail.MimeMessageHelper;
-import org.springframework.mail.javamail.MimeMessagePreparator;
-import org.springframework.stereotype.Service;
-
-@Service
-public class EmailService {
-
- private final JavaMailSender mailSender;
- @Value("${app.email.from}")
- private String from;
-
- public EmailService(JavaMailSender mailSender) {
- this.mailSender = mailSender;
- }
-
- public void send(String to, String subject, String html) {
- MimeMessagePreparator message = mimeMessage -> {
- MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
- helper.setFrom(from);
- helper.setTo(to);
- helper.setSubject(subject);
- helper.setText(html, true);
- };
- mailSender.send(message);
- }
-
-}
diff --git a/src/main/java/com/iamnbty/training/backend/service/UserService.java b/src/main/java/com/iamnbty/training/backend/service/UserService.java
index 20f3e6b..e73b1f4 100644
--- a/src/main/java/com/iamnbty/training/backend/service/UserService.java
+++ b/src/main/java/com/iamnbty/training/backend/service/UserService.java
@@ -7,6 +7,8 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
+import java.util.Calendar;
+import java.util.Date;
import java.util.Objects;
import java.util.Optional;
@@ -26,6 +28,10 @@ public Optional findById(String id) {
return repository.findById(id);
}
+ public Optional findByToken(String token) {
+ return repository.findByToken(token);
+ }
+
public Optional findByEmail(String email) {
return repository.findByEmail(email);
}
@@ -54,7 +60,7 @@ public boolean matchPassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
- public User create(String email, String password, String name) throws BaseException {
+ public User create(String email, String password, String name, String token, Date tokenExpireDate) throws BaseException {
// validate
if (Objects.isNull(email)) {
throw UserException.createEmailNull();
@@ -78,6 +84,8 @@ public User create(String email, String password, String name) throws BaseExcept
entity.setEmail(email);
entity.setPassword(passwordEncoder.encode(password));
entity.setName(name);
+ entity.setToken(token);
+ entity.setTokenExpire(tokenExpireDate);
return repository.save(entity);
}
diff --git a/src/main/java/com/iamnbty/training/backend/util/SecurityUtil.java b/src/main/java/com/iamnbty/training/backend/util/SecurityUtil.java
index 0808771..7b22ac1 100644
--- a/src/main/java/com/iamnbty/training/backend/util/SecurityUtil.java
+++ b/src/main/java/com/iamnbty/training/backend/util/SecurityUtil.java
@@ -1,9 +1,14 @@
package com.iamnbty.training.backend.util;
+import org.passay.CharacterRule;
+import org.passay.EnglishCharacterData;
+import org.passay.PasswordGenerator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
public class SecurityUtil {
@@ -32,4 +37,19 @@ public static Optional getCurrentUserId() {
return Optional.of(userId);
}
+
+ public static String generateToken() {
+ List rules = Arrays.asList(
+ new CharacterRule(EnglishCharacterData.UpperCase, 10),
+
+ new CharacterRule(EnglishCharacterData.LowerCase, 10),
+
+ new CharacterRule(EnglishCharacterData.Digit, 10)
+ );
+
+ PasswordGenerator generator = new PasswordGenerator();
+
+ return generator.generatePassword(30, rules);
+ }
+
}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index e88f180..3aa19ba 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -1,15 +1,10 @@
+server:
+ port: 8080
spring:
- mail:
- host: smtp.gmail.com
- port: 587
- username: YOUR_USERNAME
- password: YOUR_PASSWORD
- properties:
- mail:
- smtp:
- auth: true
- starttls:
- enable: true
+ kafka:
+ bootstrap-servers: localhost:9092
+ consumer:
+ group-id: "my-awesome-app"
jpa:
hibernate:
ddl-auto: update
diff --git a/src/main/resources/email/email-activate-user.html b/src/main/resources/email/email-activate-user.html
index 331b521..132a8a7 100644
--- a/src/main/resources/email/email-activate-user.html
+++ b/src/main/resources/email/email-activate-user.html
@@ -8,7 +8,7 @@
Hello, ${P_NAME}
Please activate your account
-Click here
+Click here