feat(domain-and-notification): add notifications for domain activating and renewing

This commit is contained in:
ulitsaRaskolnikova 2026-02-09 15:21:00 +03:00
parent 36df912744
commit 6c71bfc0fa
9 changed files with 190 additions and 10 deletions

View File

@ -6,5 +6,6 @@ public enum NotificationType {
DOMAIN_ACTIVATED,
DOMAIN_EXPIRING_SOON,
DOMAIN_EXPIRED,
DOMAIN_RENEWED,
EMAIL_VERIFICATION
}

View File

@ -0,0 +1,105 @@
package ru.itmo.domain.client;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import ru.itmo.common.notification.NotificationType;
import ru.itmo.common.notification.SendNotificationRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Клиент для отправки уведомлений через notification-service.
* Получает JWT-токен из текущего HTTP-запроса и пробрасывает его.
*/
@Slf4j
@Component
public class NotificationClient {
private final RestTemplate restTemplate;
private final HttpServletRequest httpServletRequest;
private final String notificationServiceUrl;
public NotificationClient(RestTemplate restTemplate,
HttpServletRequest httpServletRequest,
@Value("${services.notification.url}") String notificationServiceUrl) {
this.restTemplate = restTemplate;
this.httpServletRequest = httpServletRequest;
this.notificationServiceUrl = notificationServiceUrl;
}
public void sendDomainsActivated(UUID userId, List<String> domains, String expiresAt) {
try {
SendNotificationRequest request = new SendNotificationRequest();
request.setUserId(userId);
request.setType(NotificationType.DOMAIN_ACTIVATED);
request.setSubject("Активировано доменов: " + domains.size());
request.setParameters(Map.of(
"domains", domains,
"expiresAt", expiresAt
));
String jwtToken = extractJwtToken();
if (jwtToken == null) {
log.warn("No JWT token found, skipping notification for domains: {}", domains);
return;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(jwtToken);
HttpEntity<SendNotificationRequest> entity = new HttpEntity<>(request, headers);
String url = notificationServiceUrl + "/notifications/send";
restTemplate.exchange(url, HttpMethod.POST, entity, Void.class);
log.info("Domain activation notification sent for {} domains", domains.size());
} catch (Exception e) {
log.warn("Failed to send domain activation notification: {}", e.getMessage());
}
}
public void sendDomainsRenewed(UUID userId, Map<String, String> domainsWithExpiry) {
try {
SendNotificationRequest request = new SendNotificationRequest();
request.setUserId(userId);
request.setType(NotificationType.DOMAIN_RENEWED);
request.setSubject("Продлено доменов: " + domainsWithExpiry.size());
request.setParameters(Map.of("domains", domainsWithExpiry));
String jwtToken = extractJwtToken();
if (jwtToken == null) {
log.warn("No JWT token found, skipping renewal notification");
return;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(jwtToken);
HttpEntity<SendNotificationRequest> entity = new HttpEntity<>(request, headers);
String url = notificationServiceUrl + "/notifications/send";
restTemplate.exchange(url, HttpMethod.POST, entity, Void.class);
log.info("Domain renewal notification sent for {} domains", domainsWithExpiry.size());
} catch (Exception e) {
log.warn("Failed to send domain renewal notification: {}", e.getMessage());
}
}
private String extractJwtToken() {
String authHeader = httpServletRequest.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
}

View File

@ -3,6 +3,7 @@ package ru.itmo.domain.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.itmo.common.audit.AuditClient;
import ru.itmo.domain.client.NotificationClient;
import ru.itmo.domain.entity.Domain;
import ru.itmo.domain.exception.ForbiddenException;
import ru.itmo.domain.exception.L2DomainNotFoundException;
@ -14,6 +15,7 @@ import ru.itmo.domain.service.UserDomainService;
import ru.itmo.domain.util.SecurityUtil;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -21,12 +23,16 @@ import java.util.UUID;
@Service
public class UserDomainServiceImpl implements UserDomainService {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private final DomainRepository domainRepository;
private final AuditClient auditClient;
private final NotificationClient notificationClient;
public UserDomainServiceImpl(DomainRepository domainRepository, AuditClient auditClient) {
public UserDomainServiceImpl(DomainRepository domainRepository, AuditClient auditClient, NotificationClient notificationClient) {
this.domainRepository = domainRepository;
this.auditClient = auditClient;
this.notificationClient = notificationClient;
}
@Override
@ -106,6 +112,12 @@ public class UserDomainServiceImpl implements UserDomainService {
}
auditClient.log("Created " + createdDomains.size() + " domains (period=" + period + "): " + String.join(", ", createdDomains), userId);
// Отправляем одно email-уведомление со всеми созданными доменами
if (!createdDomains.isEmpty()) {
notificationClient.sendDomainsActivated(userId, createdDomains, finishedAt.format(DATE_FMT));
}
return createdDomains;
}
@ -119,6 +131,7 @@ public class UserDomainServiceImpl implements UserDomainService {
DomainPeriod period = request.getPeriod();
List<String> renewedDomains = new ArrayList<>();
java.util.LinkedHashMap<String, String> domainsWithExpiry = new java.util.LinkedHashMap<>();
for (String l3Domain : request.getL3Domains()) {
String l3Name = l3Domain == null ? null : l3Domain.trim();
@ -152,13 +165,20 @@ public class UserDomainServiceImpl implements UserDomainService {
if (baseDate == null || baseDate.isBefore(LocalDateTime.now())) {
baseDate = LocalDateTime.now();
}
l3.setFinishedAt(calculateFinishedAt(baseDate, period));
LocalDateTime newFinishedAt = calculateFinishedAt(baseDate, period);
l3.setFinishedAt(newFinishedAt);
domainRepository.save(l3);
renewedDomains.add(l3Name);
domainsWithExpiry.put(l3Name, newFinishedAt.format(DATE_FMT));
}
auditClient.log("Renewed " + renewedDomains.size() + " domains (period=" + period + "): " + String.join(", ", renewedDomains), userId);
if (!domainsWithExpiry.isEmpty()) {
notificationClient.sendDomainsRenewed(userId, domainsWithExpiry);
}
return renewedDomains;
}

View File

@ -16,6 +16,10 @@ exdns:
base-url: http://exdns:8000
api-token: ${EXDNS_API_TOKEN:-changeme}
services:
notification:
url: http://notification-service:8085
audit:
client:
base-url: http://audit-service:8087

View File

@ -47,6 +47,10 @@ exdns:
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-must-be-at-least-32-characters-long-for-security}
services:
notification:
url: ${NOTIFICATION_SERVICE_URL:http://localhost:8085}
audit:
client:
base-url: ${AUDIT_SERVICE_URL:http://localhost:8087}

View File

@ -21,6 +21,7 @@ public class EmailTemplateService {
case DOMAIN_ACTIVATED -> "domain-activated";
case DOMAIN_EXPIRING_SOON -> "domain-expiring-soon";
case DOMAIN_EXPIRED -> "domain-expired";
case DOMAIN_RENEWED -> "domain-renewed";
case EMAIL_VERIFICATION -> "email-verification";
};
}

View File

@ -38,6 +38,7 @@ public class NotificationService {
case DOMAIN_ACTIVATED -> "Домен активирован";
case DOMAIN_EXPIRING_SOON -> "Напоминание: срок действия домена истекает";
case DOMAIN_EXPIRED -> "Срок действия домена истек";
case DOMAIN_RENEWED -> "Домены продлены";
case EMAIL_VERIFICATION -> "Подтверждение email адреса";
};
}

View File

@ -2,15 +2,29 @@
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Домен активирован</title>
<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>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>Домены успешно активированы</h2>
<p>Здравствуйте!</p>
<p>Ваши домены были успешно активированы и готовы к использованию.</p>
<table th:if="${domains}" style="border-collapse: collapse; width: 100%; margin: 16px 0;">
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Домен</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Срок действия до</th>
</tr>
</thead>
<tbody>
<tr th:each="d : ${domains}">
<td style="border: 1px solid #ddd; padding: 8px;" th:text="${d}"></td>
<td style="border: 1px solid #ddd; padding: 8px;" th:text="${expiresAt}"></td>
</tr>
</tbody>
</table>
<p>Теперь вы можете управлять DNS-записями доменов в личном кабинете.</p>
<p>С уважением,<br>Команда Domain Registrar</p>
</div>
</body>
</html>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Домены продлены</title>
</head>
<body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>Домены успешно продлены</h2>
<p>Здравствуйте!</p>
<p>Срок действия ваших доменов был успешно продлён.</p>
<table th:if="${domains}" style="border-collapse: collapse; width: 100%; margin: 16px 0;">
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Домен</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Новый срок действия до</th>
</tr>
</thead>
<tbody>
<tr th:each="entry : ${domains}">
<td style="border: 1px solid #ddd; padding: 8px;" th:text="${entry.key}"></td>
<td style="border: 1px solid #ddd; padding: 8px;" th:text="${entry.value}"></td>
</tr>
</tbody>
</table>
<p>Вы можете управлять DNS-записями доменов в личном кабинете.</p>
<p>С уважением,<br>Команда Domain Registrar</p>
</div>
</body>
</html>