{"id":356287,"date":"2024-05-20T23:26:26","date_gmt":"2024-05-20T23:26:26","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=356287"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-05-20T23:26:26","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=356287","title":{"rendered":"<span>\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Spring Boot &amp; Angular<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442!  \u041c\u044b \u0441 \u0432\u0430\u043c\u0438 \u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c \u043e \u0432\u0430\u0436\u043d\u043e\u043c \u0430\u0441\u043f\u0435\u043a\u0442\u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u2014 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0438 \u043f\u043e\u0447\u0442\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. \u041c\u044b \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0435\u043c, \u043a\u0430\u043a \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Spring Boot \u0438 Angular, \u0434\u0432\u0443\u0445 \u043c\u043e\u0449\u043d\u044b\u0445 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439. <\/p>\n<p>\u0428\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043e\u043c \u0440\u0430\u0437\u0431\u0435\u0440\u0435\u043c\u0441\u044f, \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b \u0438 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0439 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u044f\u043c\u0438 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430. \u0422\u043e\u0433\u0434\u0430 \u043d\u0430\u0447\u043d\u0435\u043c!<\/p>\n<h2>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b7a\/9a2\/0b3\/b7a9a20b395911c389f9ca71fd4de5ab.png\" width=\"660\" height=\"554\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b7a\/9a2\/0b3\/b7a9a20b395911c389f9ca71fd4de5ab.png\"\/><\/figure>\n<p>User service \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u043d \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0438 \u0435\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435. \u0412 \u0434\u0430\u043d\u043d\u043e\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435, Apache Kafka \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 Mail Service. Mail service \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e &#171;SMTP&#187;. \u041a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443 \u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438.<\/p>\n<p><a class=\"anchor\"><\/a><\/p>\n<h2>\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435<\/h2>\n<p>\u0421\u043f\u0435\u0440\u0432\u0430, \u043d\u0430\u0434\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0431\u0440\u043e\u043a\u0435\u0440 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Apache Kafka. \u041d\u0438\u0436\u0435 \u0444\u0430\u0439\u043b Docker Compose \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0440\u0435\u0434\u044b Kafka \u0438 Zookeeper \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\u0445 Docker. \u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435, Zookeeper \u0438 Kafka \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u0440\u0442<code> 9092.<\/code> <\/p>\n<pre><code class=\"yaml\">version: '3' services:   zookeeper:     image: confluentinc\/cp-zookeeper:7.0.1     networks:       - broker-kafka     environment:       ZOOKEEPER_CLIENT_PORT: 2181       ZOOKEEPER_TICK_TIME: 2000   kafka:     image: confluentinc\/cp-kafka:7.0.1     networks:       - broker-kafka     depends_on:       - zookeeper     ports:       - \"9092:9092\"     environment:       KAFKA_BROKER_ID: 1       KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181       KAFKA_ADVERTISED_LISTENERS: PLAINTEXT:\/\/kafka:29092,PLAINTEXT_HOST:\/\/localhost:9092       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT       KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT       KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 networks:   broker-kafka:     driver: bridge<\/code><\/pre>\n<h2>User service<\/h2>\n<details class=\"spoiler\">\n<summary>application.yml<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"yaml\">server:   servlet:     context-path: \/api\/v1\/user\/   port: 3000  spring:   datasource:     url: jdbc:postgresql:\/\/localhost:5432\/habr     username: postgres     password: postgres     driver-class-name: org.postgresql.Driver   jpa:     hibernate:       ddl-auto: update   jackson:     default-property-inclusion: non_default   kafka:     producer:       key-serializer: org.apache.kafka.common.serialization.StringSerializer       value-serializer: org.springframework.kafka.support.serializer.JsonSerializer       bootstrap-servers: localhost:9092       properties:         spring:           json:             add:               type:                 headers: false<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserDTO,java<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@Data @NoArgsConstructor public class UserDTO {     private String name;     private String surname;     private String email;     private LocalDateTime time = LocalDateTime.now();       public UserDTO(String name, String surname, String email) {         this.name = name;         this.surname = surname;         this.email = email;     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>Base64Service<\/summary>\n<div class=\"spoiler__content\">\n<p>\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Base64Service \u0438 \u0435\u0433\u043e \u0438\u043c\u043f\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0438 \u0434\u0435\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0445.  <\/p>\n<pre><code class=\"java\">public interface Base64Service {      &lt;T> T decode(String data, Class&lt;T> to);      &lt;T> String encode(T t);  }<\/code><\/pre>\n<pre><code class=\"java\">@Service public class Base64ServiceImpl implements Base64Service {       private final ObjectMapper objectMapper;      public Base64ServiceImpl(             @Qualifier(\"customObjectMapper\") ObjectMapper objectMapper     ) {         this.objectMapper = objectMapper;     }      @Override     public &lt;T> T decode(String data, Class&lt;T> to) {         try {              byte[] decodedBytes = Base64.getDecoder().decode(data);              String jsonData = new String(decodedBytes);              return objectMapper.readValue(jsonData,to);          } catch (Exception e) {             throw new Base64OperationException(\"Failed to decode or convert the data\", e);         }     }      @Override     public &lt;T> String encode(T t) {         String jsonData = null;         try {             jsonData = objectMapper.writeValueAsString(t);         } catch (JsonProcessingException e) {             throw new Base64OperationException(e.getMessage());         }         return Base64.getEncoder().encodeToString(jsonData.getBytes());     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>KafkaProducer<\/summary>\n<div class=\"spoiler__content\">\n<p><code>Kafka Producer<\/code>\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c <code>KafkaTemplate<\/code>.<\/p>\n<pre><code class=\"java\">public interface KafkaProducer {     &lt;T> void produce(String topic, T t); }<\/code><\/pre>\n<pre><code class=\"java\">@Component @Slf4j @RequiredArgsConstructor public class DefaultKafkaProducer implements KafkaProducer {      private final KafkaTemplate&lt;String, Object> kafkaTemplate;      @Override     public &lt;T> void produce(String topic, T t) {         kafkaTemplate.send(                 topic, t         ).whenComplete((res, th) -> {             log.info(\"produced message: \" + res.getProducerRecord() + \" topic: \" + res.getProducerRecord().topic());         });     }  }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserContoller<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@RequiredArgsConstructor @RestController @CrossOrigin(origins = \"*\") public class UserController {      private final UserService userService;      @PostMapping(\"register\")     ResponseEntity&lt;?> requestToRegistration(             @RequestBody UserDTO userDTO     ) {         return ResponseEntity                 .ok(userService.requestToRegistration(userDTO));     }      @PostMapping(\"confirm-registration\")     ResponseEntity&lt;?> confirm(             @RequestParam String data     ) {         return ResponseEntity                 .status(201)                 .body(userService.confirmRegistration(data));     }   }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserService<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">public interface UserService {      StatusResponse requestToRegistration(UserDTO userDTO);      User confirmRegistration(String hash); }<\/code><\/pre>\n<pre><code class=\"java\">@Service @RequiredArgsConstructor public class UserServiceImpl implements UserService {      private final UserRepository userRepository;     private final Base64Service base64;     private final KafkaProducer kafkaProducer;      public static final String EMAIL_TOPIC = \"email_message\";      @Override     public StatusResponse requestToRegistration(UserDTO userDTO) {         try {             var optionalUser = this.userRepository.findByEmail(userDTO.getEmail());              if (optionalUser.isPresent()) {                 throw new EmailRegisteredException(\"email: %s registered yet\".formatted(userDTO.getEmail()));             }              var dataToSend = base64.encode(userDTO);              kafkaProducer.produce(EMAIL_TOPIC, new KafkaEmailMessageDTO(userDTO.getEmail(), dataToSend));              return new StatusResponse(                     true, null             );         } catch (Exception e) {             return new StatusResponse(                     false,                     e.getMessage()             );         }     }      @Override     public User confirmRegistration(String hash) {          var userDTO = base64.decode(hash, UserDTO.class);          if (userDTO.getTime().isBefore(LocalDateTime.now().minusDays(1))) {             throw new LinkExpiredException();         }          var user = new User(                 userDTO.getName(),                 userDTO.getSurname(),                 userDTO.getEmail()         );          return this.userRepository.save(user);     }   }<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u043e \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u0435 \u0447\u0435\u0440\u0435\u0437 24 \u0447\u0430\u0441\u0430 \u043f\u043e\u0441\u043b\u0435 \u0435\u0451 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u043e\u043d\u0430  \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0439.<\/p>\n<\/div>\n<\/details>\n<h2>Mail Service<\/h2>\n<p>\u041f\u0435\u0440\u0435\u0434 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u044d\u0442\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c credentials. <a href=\"https:\/\/medium.com\/@mhabib555\/google-smtp-username-and-password-not-accepted-error-when-sending-mail-6bb1d4f4f5f3\" rel=\"noopener noreferrer nofollow\">\u041f\u043e\u043b\u043d\u044b\u0439 \u0433\u0430\u0439\u0434<\/a>.<\/p>\n<details class=\"spoiler\">\n<summary>application.yml<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"yaml\">spring:   mail:     host: smtp.gmail.com     port: 587     username: ${SMTP_USERNAME}     password: ${SMTP_PASSWORD}     properties:       mail:         smtp:           auth: true         smtp.starttls.enable: true   kafka:     consumer:       bootstrap-servers: localhost:9092       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer       value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer       properties:         spring:           json:             add:               type:                 headers: false server:   frontend-url: http:\/\/localhost:4200<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>MailListener<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@Component @Slf4j @RequiredArgsConstructor public class MailListener {      private final MailService mailService;      @KafkaListener(             topics = \"email_message\", groupId = \"some\"     )     void listen(             KafkaMailMessage kafkaMailMessage     ) {         log.info(\"email message: {} \", kafkaMailMessage);         mailService.send(kafkaMailMessage, MessageMode.EMAIL_VERIFICATION);     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>MailService<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">public interface MailService {     void send(KafkaMailMessage kafkaMailMessage, MessageMode mode); }<\/code><\/pre>\n<pre><code class=\"java\">@Component @RequiredArgsConstructor @Slf4j public class MailServiceImpl implements MailService {      private final JavaMailSender mailSender;      @Value(\"${server.frontend-url}\")     private String frontEndURL;      @Override     public void send(KafkaMailMessage kafkaMailMessage, MessageMode mode) {         var msg = new SimpleMailMessage();          if (mode == MessageMode.EMAIL_VERIFICATION) {             msg.setText(frontEndURL + \"\/verification?data=\" + kafkaMailMessage.message());         } else {             msg.setText(kafkaMailMessage.message());         }          msg.setTo(kafkaMailMessage.email());         msg.setFrom(\"habrexample@gmail.com\");          try {             mailSender.send(msg);             log.info(\"email send, msg: {}, mode: {}\", kafkaMailMessage, mode);         } catch (Exception e) {             log.error(\"send mail error : {}\", e.getMessage());         }       } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<hr\/>\n<h2>Angular Client<\/h2>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9fb\/3eb\/c8a\/9fb3ebc8aa1a6afd097af1e4a0058874.png\" alt=\"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:\" title=\"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:\" width=\"378\" height=\"609\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/9fb\/3eb\/c8a\/9fb3ebc8aa1a6afd097af1e4a0058874.png\"\/><\/p>\n<div><figcaption>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/figcaption><\/div>\n<\/figure>\n<details class=\"spoiler\">\n<summary>Registration Component<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">@Component({   selector: 'app-registration',   templateUrl: '.\/registration.component.html',   styleUrls: ['.\/registration.component.css'] }) export class RegistrationComponent {    registrationForm: FormGroup;    constructor(     private userService: UserService,     private formBuilder: FormBuilder   ) {     this.registrationForm = this.formBuilder.group({       name: ['', Validators.required],       surname: ['', Validators.required],       email: ['', [Validators.required, Validators.email]],     });   }    register() {     let name = this.findInRegistrationForm('name')     let surname = this.findInRegistrationForm('surname')     let email = this.findInRegistrationForm('email')      let userDTO = {name, surname, email}      this.userService.requestToRegistration(userDTO)       .subscribe((res: StatusResponse) => {           alert(JSON.stringify(res))         }       )   }    private findInRegistrationForm(     controlName: string   ) {     return this.registrationForm.get(controlName)?.value as string   }   }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0437\u0430\u043c\u0435\u0442\u0438\u043b\u0438, \u043c\u044b \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u0441\u044b\u043b\u043a\u0443 \u0432 \u0442\u0430\u043a\u043e\u043c \u0448\u0430\u0431\u043b\u043e\u043d\u0435: http:\/\/localhost:4200\/verification?data={dataFromKafkaMailMessage}<\/p>\n<details class=\"spoiler\">\n<summary>Verification Component<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import {Component, OnInit} from '@angular\/core'; import {ActivatedRoute, Router} from \"@angular\/router\"; import {User} from \"..\/..\/model\/User\"; import {UserService} from \"..\/..\/service\/user.service\";  @Component({   selector: 'app-verification',   templateUrl: '.\/verification.component.html',   styleUrls: ['.\/verification.component.css'] }) export class VerificationComponent implements OnInit {    constructor(     private route: ActivatedRoute,     private userService: UserService   ) {   }    user: User    ngOnInit(): void {     this.route.queryParams.subscribe(params => {       let data = params['data'] || null;        if (data) {         this.userService.confirmRegistration(data)           .subscribe(res => {             this.user = res             console.log(res)           }, err => {             if (err) {               alert('invalid confirmation link');             }           })       } else {         alert('missing data')       }     })   }   }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/cce\/891\/8d0\/cce8918d0d0acd22c5f01ef048e0a669.jpg\" width=\"562\" height=\"694\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/cce\/891\/8d0\/cce8918d0d0acd22c5f01ef048e0a669.jpg\" data-blurred=\"true\"\/><\/figure>\n<p>Success \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 mail. <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6f0\/e49\/f8a\/6f0e49f8a4b9fb34ad91edb1d30d17b4.png\" width=\"872\" height=\"312\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/6f0\/e49\/f8a\/6f0e49f8a4b9fb34ad91edb1d30d17b4.png\"\/><\/figure>\n<p>\u0412\u044b\u0448\u0435 \u0441\u043a\u0440\u0438\u043d\u0448\u043e\u0442, \u0442\u043e\u0433\u043e \u043a\u0430\u043a \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0432 gmail. \u041f\u043e \u043a\u043b\u0438\u043a\u0443 \u043c\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u043c \u0432 verification component, \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441 URL \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 user service \u0432 \u0431\u044d\u043a\u0435\u043d\u0434.<\/p>\n<details class=\"spoiler\">\n<summary>UserService<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">@Injectable({   providedIn: 'root' }) export class UserService {    private http = inject(HttpClient)    private BASE_URL = 'http:\/\/localhost:3000\/api\/v1\/user';    requestToRegistration(     userDTO: UserDTO   ): Observable&lt;any> {     return this.http       .post(`${this.BASE_URL}\/register`, userDTO);   }    confirmRegistration(     data: string   ): Observable&lt;any> {     return this.http       .post(`${this.BASE_URL}\/confirm-registration?data=` + data, {});   }  }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/39b\/ca9\/da3\/39bca9da3509d13314778c351894bfda.png\" width=\"403\" height=\"133\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/39b\/ca9\/da3\/39bca9da3509d13314778c351894bfda.png\"\/><\/figure>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p>\u041c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043b\u0438 \u0432\u0430\u0436\u043d\u044b\u0439 \u0430\u0441\u043f\u0435\u043a\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 &#8212; \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0432 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u0445. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u043c\u044b \u0433\u043e\u0442\u043e\u0432\u044b \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u043e\u0441\u0442\u044c \u0438 \u0437\u0430\u0449\u0438\u0442\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.<\/p>\n<p>\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 <a href=\"https:\/\/github.com\/youngAndMad\/mail-verification-habr\" rel=\"noopener noreferrer nofollow\">Github<\/a>.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/764458\/\"> https:\/\/habr.com\/ru\/articles\/764458\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442!  \u041c\u044b \u0441 \u0432\u0430\u043c\u0438 \u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c \u043e \u0432\u0430\u0436\u043d\u043e\u043c \u0430\u0441\u043f\u0435\u043a\u0442\u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u2014 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0438 \u043f\u043e\u0447\u0442\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. \u041c\u044b \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0435\u043c, \u043a\u0430\u043a \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Spring Boot \u0438 Angular, \u0434\u0432\u0443\u0445 \u043c\u043e\u0449\u043d\u044b\u0445 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439. <\/p>\n<p>\u0428\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043e\u043c \u0440\u0430\u0437\u0431\u0435\u0440\u0435\u043c\u0441\u044f, \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b \u0438 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0439 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u044f\u043c\u0438 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430. \u0422\u043e\u0433\u0434\u0430 \u043d\u0430\u0447\u043d\u0435\u043c!<\/p>\n<h2>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<figure class=\"full-width\"><\/figure>\n<p>User service \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u043d \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0438 \u0435\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435. \u0412 \u0434\u0430\u043d\u043d\u043e\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435, Apache Kafka \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 Mail Service. Mail service \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e &#171;SMTP&#187;. \u041a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443 \u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438.<\/p>\n<p><a class=\"anchor\"><\/a><\/p>\n<h2>\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435<\/h2>\n<p>\u0421\u043f\u0435\u0440\u0432\u0430, \u043d\u0430\u0434\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0431\u0440\u043e\u043a\u0435\u0440 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Apache Kafka. \u041d\u0438\u0436\u0435 \u0444\u0430\u0439\u043b Docker Compose \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0440\u0435\u0434\u044b Kafka \u0438 Zookeeper \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\u0445 Docker. \u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435, Zookeeper \u0438 Kafka \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u0440\u0442<code> 9092.<\/code> <\/p>\n<pre><code class=\"yaml\">version: '3' services:   zookeeper:     image: confluentinc\/cp-zookeeper:7.0.1     networks:       - broker-kafka     environment:       ZOOKEEPER_CLIENT_PORT: 2181       ZOOKEEPER_TICK_TIME: 2000   kafka:     image: confluentinc\/cp-kafka:7.0.1     networks:       - broker-kafka     depends_on:       - zookeeper     ports:       - \"9092:9092\"     environment:       KAFKA_BROKER_ID: 1       KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181       KAFKA_ADVERTISED_LISTENERS: PLAINTEXT:\/\/kafka:29092,PLAINTEXT_HOST:\/\/localhost:9092       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT       KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT       KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 networks:   broker-kafka:     driver: bridge<\/code><\/pre>\n<h2>User service<\/h2>\n<details class=\"spoiler\">\n<summary>application.yml<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"yaml\">server:   servlet:     context-path: \/api\/v1\/user\/   port: 3000  spring:   datasource:     url: jdbc:postgresql:\/\/localhost:5432\/habr     username: postgres     password: postgres     driver-class-name: org.postgresql.Driver   jpa:     hibernate:       ddl-auto: update   jackson:     default-property-inclusion: non_default   kafka:     producer:       key-serializer: org.apache.kafka.common.serialization.StringSerializer       value-serializer: org.springframework.kafka.support.serializer.JsonSerializer       bootstrap-servers: localhost:9092       properties:         spring:           json:             add:               type:                 headers: false<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserDTO,java<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@Data @NoArgsConstructor public class UserDTO {     private String name;     private String surname;     private String email;     private LocalDateTime time = LocalDateTime.now();       public UserDTO(String name, String surname, String email) {         this.name = name;         this.surname = surname;         this.email = email;     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>Base64Service<\/summary>\n<div class=\"spoiler__content\">\n<p>\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Base64Service \u0438 \u0435\u0433\u043e \u0438\u043c\u043f\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0438 \u0434\u0435\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0445.  <\/p>\n<pre><code class=\"java\">public interface Base64Service {      &lt;T> T decode(String data, Class&lt;T> to);      &lt;T> String encode(T t);  }<\/code><\/pre>\n<pre><code class=\"java\">@Service public class Base64ServiceImpl implements Base64Service {       private final ObjectMapper objectMapper;      public Base64ServiceImpl(             @Qualifier(\"customObjectMapper\") ObjectMapper objectMapper     ) {         this.objectMapper = objectMapper;     }      @Override     public &lt;T> T decode(String data, Class&lt;T> to) {         try {              byte[] decodedBytes = Base64.getDecoder().decode(data);              String jsonData = new String(decodedBytes);              return objectMapper.readValue(jsonData,to);          } catch (Exception e) {             throw new Base64OperationException(\"Failed to decode or convert the data\", e);         }     }      @Override     public &lt;T> String encode(T t) {         String jsonData = null;         try {             jsonData = objectMapper.writeValueAsString(t);         } catch (JsonProcessingException e) {             throw new Base64OperationException(e.getMessage());         }         return Base64.getEncoder().encodeToString(jsonData.getBytes());     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>KafkaProducer<\/summary>\n<div class=\"spoiler__content\">\n<p><code>Kafka Producer<\/code>\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c <code>KafkaTemplate<\/code>.<\/p>\n<pre><code class=\"java\">public interface KafkaProducer {     &lt;T> void produce(String topic, T t); }<\/code><\/pre>\n<pre><code class=\"java\">@Component @Slf4j @RequiredArgsConstructor public class DefaultKafkaProducer implements KafkaProducer {      private final KafkaTemplate&lt;String, Object> kafkaTemplate;      @Override     public &lt;T> void produce(String topic, T t) {         kafkaTemplate.send(                 topic, t         ).whenComplete((res, th) -> {             log.info(\"produced message: \" + res.getProducerRecord() + \" topic: \" + res.getProducerRecord().topic());         });     }  }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserContoller<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@RequiredArgsConstructor @RestController @CrossOrigin(origins = \"*\") public class UserController {      private final UserService userService;      @PostMapping(\"register\")     ResponseEntity&lt;?> requestToRegistration(             @RequestBody UserDTO userDTO     ) {         return ResponseEntity                 .ok(userService.requestToRegistration(userDTO));     }      @PostMapping(\"confirm-registration\")     ResponseEntity&lt;?> confirm(             @RequestParam String data     ) {         return ResponseEntity                 .status(201)                 .body(userService.confirmRegistration(data));     }   }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>UserService<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">public interface UserService {      StatusResponse requestToRegistration(UserDTO userDTO);      User confirmRegistration(String hash); }<\/code><\/pre>\n<pre><code class=\"java\">@Service @RequiredArgsConstructor public class UserServiceImpl implements UserService {      private final UserRepository userRepository;     private final Base64Service base64;     private final KafkaProducer kafkaProducer;      public static final String EMAIL_TOPIC = \"email_message\";      @Override     public StatusResponse requestToRegistration(UserDTO userDTO) {         try {             var optionalUser = this.userRepository.findByEmail(userDTO.getEmail());              if (optionalUser.isPresent()) {                 throw new EmailRegisteredException(\"email: %s registered yet\".formatted(userDTO.getEmail()));             }              var dataToSend = base64.encode(userDTO);              kafkaProducer.produce(EMAIL_TOPIC, new KafkaEmailMessageDTO(userDTO.getEmail(), dataToSend));              return new StatusResponse(                     true, null             );         } catch (Exception e) {             return new StatusResponse(                     false,                     e.getMessage()             );         }     }      @Override     public User confirmRegistration(String hash) {          var userDTO = base64.decode(hash, UserDTO.class);          if (userDTO.getTime().isBefore(LocalDateTime.now().minusDays(1))) {             throw new LinkExpiredException();         }          var user = new User(                 userDTO.getName(),                 userDTO.getSurname(),                 userDTO.getEmail()         );          return this.userRepository.save(user);     }   }<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043f\u043e \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u0435 \u0447\u0435\u0440\u0435\u0437 24 \u0447\u0430\u0441\u0430 \u043f\u043e\u0441\u043b\u0435 \u0435\u0451 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u043e\u043d\u0430  \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0439.<\/p>\n<\/div>\n<\/details>\n<h2>Mail Service<\/h2>\n<p>\u041f\u0435\u0440\u0435\u0434 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u044d\u0442\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c credentials. <a href=\"https:\/\/medium.com\/@mhabib555\/google-smtp-username-and-password-not-accepted-error-when-sending-mail-6bb1d4f4f5f3\" rel=\"noopener noreferrer nofollow\">\u041f\u043e\u043b\u043d\u044b\u0439 \u0433\u0430\u0439\u0434<\/a>.<\/p>\n<details class=\"spoiler\">\n<summary>application.yml<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"yaml\">spring:   mail:     host: smtp.gmail.com     port: 587     username: ${SMTP_USERNAME}     password: ${SMTP_PASSWORD}     properties:       mail:         smtp:           auth: true         smtp.starttls.enable: true   kafka:     consumer:       bootstrap-servers: localhost:9092       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer       value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer       properties:         spring:           json:             add:               type:                 headers: false server:   frontend-url: http:\/\/localhost:4200<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>MailListener<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">@Component @Slf4j @RequiredArgsConstructor public class MailListener {      private final MailService mailService;      @KafkaListener(             topics = \"email_message\", groupId = \"some\"     )     void listen(             KafkaMailMessage kafkaMailMessage     ) {         log.info(\"email message: {} \", kafkaMailMessage);         mailService.send(kafkaMailMessage, MessageMode.EMAIL_VERIFICATION);     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>MailService<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"java\">public interface MailService {     void send(KafkaMailMessage kafkaMailMessage, MessageMode mode); }<\/code><\/pre>\n<pre><code class=\"java\">@Component @RequiredArgsConstructor @Slf4j public class MailServiceImpl implements MailService {      private final JavaMailSender mailSender;      @Value(\"${server.frontend-url}\")     private String frontEndURL;      @Override     public void send(KafkaMailMessage kafkaMailMessage, MessageMode mode) {         var msg = new SimpleMailMessage();          if (mode == MessageMode.EMAIL_VERIFICATION) {             msg.setText(frontEndURL + \"\/verification?data=\" + kafkaMailMessage.message());         } else {             msg.setText(kafkaMailMessage.message());         }          msg.setTo(kafkaMailMessage.email());         msg.setFrom(\"habrexample@gmail.com\");          try {             mailSender.send(msg);             log.info(\"email send, msg: {}, mode: {}\", kafkaMailMessage, mode);         } catch (Exception e) {             log.error(\"send mail error : {}\", e.getMessage());         }       } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<hr\/>\n<h2>Angular Client<\/h2>\n<figure class=\"\">\n<div><figcaption>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/figcaption><\/div>\n<\/figure>\n<details class=\"spoiler\">\n<summary>Registration Component<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">@Component({   selector: 'app-registration',   templateUrl: '.\/registration.component.html',   styleUrls: ['.\/registration.component.css'] }) export class RegistrationComponent {    registrationForm: FormGroup;    constructor(     private userService: UserService,     private formBuilder: FormBuilder   ) {     this.registrationForm = this.formBuilder.group({       name: ['', Validators.required],       surname: ['', Validators.required],       email: ['', [Validators.required, Validators.email]],     });   }    register() {     let name =<\/code><\/pre>\n<\/div>\n<\/details>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-356287","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/356287","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=356287"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/356287\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=356287"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=356287"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=356287"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}