mirror of
https://github.com/ulitsaRaskolnikova/is-coursework.git
synced 2026-09-14 14:05:36 +05:00
feat(notification): add email sending
This commit is contained in:
parent
e2f41ebc9e
commit
1ae10a3f85
@ -1,5 +1,6 @@
|
||||
package ru.itmo.common.notification;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@ -13,8 +14,12 @@ import java.util.UUID;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SendNotificationRequest {
|
||||
@NotNull(message = "User ID is required")
|
||||
private UUID userId;
|
||||
|
||||
@NotNull(message = "Notification type is required")
|
||||
private NotificationType type;
|
||||
|
||||
private String subject;
|
||||
private String template;
|
||||
private Map<String, Object> parameters;
|
||||
|
||||
@ -61,6 +61,10 @@ services:
|
||||
- "8085:8085"
|
||||
environment:
|
||||
- SPRING_PROFILES_ACTIVE=docker
|
||||
- MAIL_HOST=${MAIL_HOST:-smtp.yandex.ru}
|
||||
- MAIL_PORT=${MAIL_PORT:-587}
|
||||
- MAIL_USERNAME=${MAIL_USERNAME:-}
|
||||
- MAIL_PASSWORD=${MAIL_PASSWORD:-}
|
||||
depends_on:
|
||||
- postgres-notification
|
||||
networks:
|
||||
|
||||
@ -8,9 +8,7 @@ COPY common/build.gradle.kts ./common/
|
||||
COPY domain-order-service/src ./domain-order-service/src
|
||||
COPY common/src ./common/src
|
||||
|
||||
RUN gradle :domain-order-service:bootJar --no-daemon \
|
||||
--refresh-dependencies \
|
||||
-Dorg.gradle.jvmargs="-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Dhttps.protocols=TLSv1.2,TLSv1.3"
|
||||
RUN gradle :domain-order-service:bootJar --no-daemon
|
||||
|
||||
FROM eclipse-temurin:17-jre-jammy
|
||||
WORKDIR /app
|
||||
|
||||
@ -15,6 +15,8 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
implementation("org.liquibase:liquibase-core")
|
||||
implementation("com.zaxxer:HikariCP")
|
||||
implementation(project(":common"))
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package ru.itmo.notification.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,25 @@
|
||||
package ru.itmo.notification.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.itmo.common.dto.ApiResponse;
|
||||
import ru.itmo.common.notification.SendNotificationRequest;
|
||||
|
||||
import java.util.UUID;
|
||||
import ru.itmo.notification.service.NotificationService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/notifications")
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationController {
|
||||
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<ApiResponse<Void>> sendNotification(
|
||||
@RequestBody SendNotificationRequest request) {
|
||||
// TODO: Implement notification sending logic
|
||||
// - Validate request
|
||||
// - Get user email from Auth Service
|
||||
// - Send email via SMTP/Email API
|
||||
// - Log notification status
|
||||
@Valid @RequestBody SendNotificationRequest request) {
|
||||
notificationService.sendNotification(request);
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package ru.itmo.notification.exception;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import ru.itmo.common.dto.ApiError;
|
||||
import ru.itmo.common.dto.ApiResponse;
|
||||
import ru.itmo.notification.service.EmailSendingException;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class EmailSendingExceptionHandler {
|
||||
|
||||
@ExceptionHandler(EmailSendingException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleEmailSendingException(EmailSendingException e) {
|
||||
log.error("Email sending failed", e);
|
||||
ApiError error = new ApiError(
|
||||
"EMAIL_SENDING_FAILED",
|
||||
"Failed to send email: " + e.getMessage()
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error(error));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.error("Invalid request", e);
|
||||
ApiError error = new ApiError(
|
||||
"INVALID_REQUEST",
|
||||
e.getMessage()
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error(error));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package ru.itmo.notification.service;
|
||||
|
||||
public class EmailSendingException extends RuntimeException {
|
||||
public EmailSendingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EmailSendingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package ru.itmo.notification.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.itmo.common.notification.NotificationType;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class EmailService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final EmailTemplateService emailTemplateService;
|
||||
|
||||
@Value("${spring.mail.from:noreply@hrofors.ru}")
|
||||
private String mailFrom;
|
||||
|
||||
public void sendEmail(String to, String subject, String text) {
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setTo(to);
|
||||
message.setSubject(subject);
|
||||
message.setText(text);
|
||||
message.setFrom(mailFrom);
|
||||
mailSender.send(message);
|
||||
log.info("Email sent successfully to: {}", to);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send email to: {}", to, e);
|
||||
throw new EmailSendingException("Failed to send email", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendHtmlEmail(String to, String subject, String htmlContent) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlContent, true);
|
||||
helper.setFrom(mailFrom);
|
||||
mailSender.send(message);
|
||||
log.info("HTML email sent successfully to: {}", to);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send HTML email to: {}", to, e);
|
||||
throw new EmailSendingException("Failed to send HTML email", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNotification(String to, NotificationType type, String subject, Map<String, Object> parameters) {
|
||||
try {
|
||||
String templateName = emailTemplateService.getTemplateName(type);
|
||||
String htmlContent = emailTemplateService.processTemplate(templateName, parameters);
|
||||
sendHtmlEmail(to, subject, htmlContent);
|
||||
log.info("Notification email sent: type={}, to={}", type, to);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send notification email: type={}, to={}", type, to, e);
|
||||
throw new EmailSendingException("Failed to send notification email", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package ru.itmo.notification.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import ru.itmo.common.notification.NotificationType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailTemplateService {
|
||||
|
||||
private final TemplateEngine templateEngine;
|
||||
|
||||
public String getTemplateName(NotificationType type) {
|
||||
return switch (type) {
|
||||
case ORDER_CREATED -> "order-created";
|
||||
case PAYMENT_APPROVED -> "payment-approved";
|
||||
case DOMAIN_ACTIVATED -> "domain-activated";
|
||||
case DOMAIN_EXPIRING_SOON -> "domain-expiring-soon";
|
||||
case DOMAIN_EXPIRED -> "domain-expired";
|
||||
};
|
||||
}
|
||||
|
||||
public String processTemplate(String templateName, Map<String, Object> parameters) {
|
||||
Context context = new Context();
|
||||
if (parameters != null) {
|
||||
parameters.forEach(context::setVariable);
|
||||
}
|
||||
return templateEngine.process("emails/" + templateName, context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package ru.itmo.notification.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import ru.itmo.common.notification.NotificationType;
|
||||
import ru.itmo.common.notification.SendNotificationRequest;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class NotificationService {
|
||||
|
||||
private final EmailService emailService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final String authServiceUrl = "http://auth-service:8081";
|
||||
|
||||
public void sendNotification(SendNotificationRequest request) {
|
||||
// String userEmail = getUserEmail(request.getUserId());
|
||||
String userEmail = "malyshev.2005n@gmail.com";
|
||||
|
||||
if (userEmail == null || userEmail.isEmpty()) {
|
||||
log.warn("User email not found for userId: {}", request.getUserId());
|
||||
throw new IllegalArgumentException("User email not found");
|
||||
}
|
||||
|
||||
String subject = request.getSubject() != null
|
||||
? request.getSubject()
|
||||
: getDefaultSubject(request.getType());
|
||||
|
||||
emailService.sendNotification(
|
||||
userEmail,
|
||||
request.getType(),
|
||||
subject,
|
||||
request.getParameters()
|
||||
);
|
||||
}
|
||||
|
||||
private String getUserEmail(UUID userId) {
|
||||
try {
|
||||
String url = authServiceUrl + "/api/auth/users/" + userId + "/email";
|
||||
String email = restTemplate.getForObject(url, String.class);
|
||||
log.debug("Retrieved email for userId {}: {}", userId, email);
|
||||
return email;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get user email for userId: {}", userId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getDefaultSubject(NotificationType type) {
|
||||
return switch (type) {
|
||||
case ORDER_CREATED -> "Заказ создан";
|
||||
case PAYMENT_APPROVED -> "Платеж успешно обработан";
|
||||
case DOMAIN_ACTIVATED -> "Домен активирован";
|
||||
case DOMAIN_EXPIRING_SOON -> "Напоминание: срок действия домена истекает";
|
||||
case DOMAIN_EXPIRED -> "Срок действия домена истек";
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -2,4 +2,19 @@ spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://postgres-notification:5432/notification_db
|
||||
username: postgres
|
||||
password: postgres
|
||||
password: postgres
|
||||
mail:
|
||||
host: ${MAIL_HOST:smtp.gmail.com}
|
||||
port: ${MAIL_PORT:587}
|
||||
username: ${MAIL_USERNAME:}
|
||||
password: ${MAIL_PASSWORD:}
|
||||
from: ${MAIL_FROM:noreply@hrofors.ru}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
starttls:
|
||||
enable: true
|
||||
required: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 5000
|
||||
writetimeout: 5000
|
||||
@ -19,6 +19,27 @@ spring:
|
||||
leak-detection-threshold: 60000
|
||||
liquibase:
|
||||
change-log: classpath:db/changelog/db.changelog-master.yaml
|
||||
mail:
|
||||
host: postbox.cloud.yandex.net
|
||||
port: 587
|
||||
username: ${MAIL_USERNAME:}
|
||||
password: ${MAIL_PASSWORD:}
|
||||
from: ${MAIL_FROM:noreply@hrofors.ru}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
required: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 5000
|
||||
writetimeout: 5000
|
||||
thymeleaf:
|
||||
prefix: classpath:/templates/
|
||||
suffix: .html
|
||||
mode: HTML
|
||||
encoding: UTF-8
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Домен активирован</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Домен успешно активирован</h2>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Ваш домен был успешно активирован и готов к использованию.</p>
|
||||
<p th:if="${domainName}">Домен: <strong th:text="${domainName}"></strong></p>
|
||||
<p th:if="${expiresAt}">Срок действия до: <strong th:text="${expiresAt}"></strong></p>
|
||||
<p>Теперь вы можете управлять DNS-записями домена в личном кабинете.</p>
|
||||
<p>С уважением,<br>Команда Domain Registrar</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Срок действия домена истек</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Срок действия домена истек</h2>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Срок действия вашего домена истек.</p>
|
||||
<p th:if="${domainName}">Домен: <strong th:text="${domainName}"></strong></p>
|
||||
<p th:if="${expiresAt}">Срок действия истек: <strong th:text="${expiresAt}"></strong></p>
|
||||
<p>Домен был деактивирован. Для восстановления доступа необходимо продлить регистрацию домена.</p>
|
||||
<p>Продлите домен в личном кабинете как можно скорее.</p>
|
||||
<p>С уважением,<br>Команда Domain Registrar</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Напоминание: срок действия домена истекает</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Напоминание о продлении домена</h2>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Срок действия вашего домена скоро истечет.</p>
|
||||
<p th:if="${domainName}">Домен: <strong th:text="${domainName}"></strong></p>
|
||||
<p th:if="${expiresAt}">Срок действия до: <strong th:text="${expiresAt}"></strong></p>
|
||||
<p th:if="${daysLeft}">Осталось дней: <strong th:text="${daysLeft}"></strong></p>
|
||||
<p>Чтобы продолжить использование домена, необходимо продлить его регистрацию.</p>
|
||||
<p>Продлите домен в личном кабинете, чтобы избежать его деактивации.</p>
|
||||
<p>С уважением,<br>Команда Domain Registrar</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Заказ создан</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Заказ успешно создан</h2>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Ваш заказ был успешно создан.</p>
|
||||
<p th:if="${orderId}">Номер заказа: <strong th:text="${orderId}"></strong></p>
|
||||
<p th:if="${totalAmount}">Сумма заказа: <strong th:text="${totalAmount}"></strong> руб.</p>
|
||||
<p>Для оплаты заказа перейдите по ссылке в личном кабинете.</p>
|
||||
<p>С уважением,<br>Команда Domain Registrar</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Платеж успешно обработан</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Платеж успешно обработан</h2>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Ваш платеж был успешно обработан.</p>
|
||||
<p th:if="${orderId}">Номер заказа: <strong th:text="${orderId}"></strong></p>
|
||||
<p th:if="${amount}">Сумма платежа: <strong th:text="${amount}"></strong> руб.</p>
|
||||
<p th:if="${domainName}">Домен <strong th:text="${domainName}"></strong> будет активирован в ближайшее время.</p>
|
||||
<p>С уважением,<br>Команда Domain Registrar</p>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user