diff --git a/common/src/main/java/ru/itmo/common/notification/SendNotificationRequest.java b/common/src/main/java/ru/itmo/common/notification/SendNotificationRequest.java index 98e0871..037c266 100644 --- a/common/src/main/java/ru/itmo/common/notification/SendNotificationRequest.java +++ b/common/src/main/java/ru/itmo/common/notification/SendNotificationRequest.java @@ -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 parameters; diff --git a/docker-compose.yml b/docker-compose.yml index 3585ec2..a530301 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/domain-order-service/Dockerfile b/domain-order-service/Dockerfile index f27d7d0..9a22b85 100644 --- a/domain-order-service/Dockerfile +++ b/domain-order-service/Dockerfile @@ -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 diff --git a/notification-service/build.gradle.kts b/notification-service/build.gradle.kts index 1e90c89..e9ccaa3 100644 --- a/notification-service/build.gradle.kts +++ b/notification-service/build.gradle.kts @@ -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")) diff --git a/notification-service/src/main/java/ru/itmo/notification/config/RestTemplateConfig.java b/notification-service/src/main/java/ru/itmo/notification/config/RestTemplateConfig.java new file mode 100644 index 0000000..59109b4 --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/config/RestTemplateConfig.java @@ -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(); + } +} diff --git a/notification-service/src/main/java/ru/itmo/notification/controller/NotificationController.java b/notification-service/src/main/java/ru/itmo/notification/controller/NotificationController.java index f1239e6..2811388 100644 --- a/notification-service/src/main/java/ru/itmo/notification/controller/NotificationController.java +++ b/notification-service/src/main/java/ru/itmo/notification/controller/NotificationController.java @@ -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> 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)); } diff --git a/notification-service/src/main/java/ru/itmo/notification/exception/EmailSendingExceptionHandler.java b/notification-service/src/main/java/ru/itmo/notification/exception/EmailSendingExceptionHandler.java new file mode 100644 index 0000000..8bac7fc --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/exception/EmailSendingExceptionHandler.java @@ -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> 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> 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)); + } +} diff --git a/notification-service/src/main/java/ru/itmo/notification/service/EmailSendingException.java b/notification-service/src/main/java/ru/itmo/notification/service/EmailSendingException.java new file mode 100644 index 0000000..8417814 --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/service/EmailSendingException.java @@ -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); + } +} diff --git a/notification-service/src/main/java/ru/itmo/notification/service/EmailService.java b/notification-service/src/main/java/ru/itmo/notification/service/EmailService.java new file mode 100644 index 0000000..68fa3bd --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/service/EmailService.java @@ -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 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); + } + } +} diff --git a/notification-service/src/main/java/ru/itmo/notification/service/EmailTemplateService.java b/notification-service/src/main/java/ru/itmo/notification/service/EmailTemplateService.java new file mode 100644 index 0000000..eceff1a --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/service/EmailTemplateService.java @@ -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 parameters) { + Context context = new Context(); + if (parameters != null) { + parameters.forEach(context::setVariable); + } + return templateEngine.process("emails/" + templateName, context); + } +} diff --git a/notification-service/src/main/java/ru/itmo/notification/service/NotificationService.java b/notification-service/src/main/java/ru/itmo/notification/service/NotificationService.java new file mode 100644 index 0000000..276df9a --- /dev/null +++ b/notification-service/src/main/java/ru/itmo/notification/service/NotificationService.java @@ -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 -> "Срок действия домена истек"; + }; + } +} diff --git a/notification-service/src/main/resources/application-docker.yml b/notification-service/src/main/resources/application-docker.yml index 0d7067a..07859b7 100644 --- a/notification-service/src/main/resources/application-docker.yml +++ b/notification-service/src/main/resources/application-docker.yml @@ -2,4 +2,19 @@ spring: datasource: url: jdbc:postgresql://postgres-notification:5432/notification_db username: postgres - password: postgres \ No newline at end of file + 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 \ No newline at end of file diff --git a/notification-service/src/main/resources/application.yml b/notification-service/src/main/resources/application.yml index bc8c39d..9138e83 100644 --- a/notification-service/src/main/resources/application.yml +++ b/notification-service/src/main/resources/application.yml @@ -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 diff --git a/notification-service/src/main/resources/templates/emails/domain-activated.html b/notification-service/src/main/resources/templates/emails/domain-activated.html new file mode 100644 index 0000000..ba82fbf --- /dev/null +++ b/notification-service/src/main/resources/templates/emails/domain-activated.html @@ -0,0 +1,16 @@ + + + + + Домен активирован + + +

Домен успешно активирован

+

Здравствуйте!

+

Ваш домен был успешно активирован и готов к использованию.

+

Домен:

+

Срок действия до:

+

Теперь вы можете управлять DNS-записями домена в личном кабинете.

+

С уважением,
Команда Domain Registrar

+ + diff --git a/notification-service/src/main/resources/templates/emails/domain-expired.html b/notification-service/src/main/resources/templates/emails/domain-expired.html new file mode 100644 index 0000000..ea1d608 --- /dev/null +++ b/notification-service/src/main/resources/templates/emails/domain-expired.html @@ -0,0 +1,17 @@ + + + + + Срок действия домена истек + + +

Срок действия домена истек

+

Здравствуйте!

+

Срок действия вашего домена истек.

+

Домен:

+

Срок действия истек:

+

Домен был деактивирован. Для восстановления доступа необходимо продлить регистрацию домена.

+

Продлите домен в личном кабинете как можно скорее.

+

С уважением,
Команда Domain Registrar

+ + diff --git a/notification-service/src/main/resources/templates/emails/domain-expiring-soon.html b/notification-service/src/main/resources/templates/emails/domain-expiring-soon.html new file mode 100644 index 0000000..eb2c405 --- /dev/null +++ b/notification-service/src/main/resources/templates/emails/domain-expiring-soon.html @@ -0,0 +1,18 @@ + + + + + Напоминание: срок действия домена истекает + + +

Напоминание о продлении домена

+

Здравствуйте!

+

Срок действия вашего домена скоро истечет.

+

Домен:

+

Срок действия до:

+

Осталось дней:

+

Чтобы продолжить использование домена, необходимо продлить его регистрацию.

+

Продлите домен в личном кабинете, чтобы избежать его деактивации.

+

С уважением,
Команда Domain Registrar

+ + diff --git a/notification-service/src/main/resources/templates/emails/order-created.html b/notification-service/src/main/resources/templates/emails/order-created.html new file mode 100644 index 0000000..619edab --- /dev/null +++ b/notification-service/src/main/resources/templates/emails/order-created.html @@ -0,0 +1,16 @@ + + + + + Заказ создан + + +

Заказ успешно создан

+

Здравствуйте!

+

Ваш заказ был успешно создан.

+

Номер заказа:

+

Сумма заказа: руб.

+

Для оплаты заказа перейдите по ссылке в личном кабинете.

+

С уважением,
Команда Domain Registrar

+ + diff --git a/notification-service/src/main/resources/templates/emails/payment-approved.html b/notification-service/src/main/resources/templates/emails/payment-approved.html new file mode 100644 index 0000000..36d6599 --- /dev/null +++ b/notification-service/src/main/resources/templates/emails/payment-approved.html @@ -0,0 +1,16 @@ + + + + + Платеж успешно обработан + + +

Платеж успешно обработан

+

Здравствуйте!

+

Ваш платеж был успешно обработан.

+

Номер заказа:

+

Сумма платежа: руб.

+

Домен будет активирован в ближайшее время.

+

С уважением,
Команда Domain Registrar

+ +