diff --git a/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java new file mode 100644 index 0000000..a43f2aa --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java @@ -0,0 +1,88 @@ +package ru.micord.ervu.audit.config; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SaslConfigs; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaAdmin; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +/** + * @author Adel Kalimullin + */ +@Configuration +public class AuditKafkaConfig { + @Value("${audit.kafka.bootstrap.servers}") + private String bootstrapServers; + @Value("${audit.kafka.security.protocol}") + private String securityProtocol; + @Value("${audit.kafka.login.module:org.apache.kafka.common.security.scram.ScramLoginModule}") + private String loginModule; + @Value("${audit.kafka.username}") + private String username; + @Value("${audit.kafka.password}") + private String password; + @Value("${audit.kafka.sasl.mechanism}") + private String saslMechanism; + @Value("${audit.kafka.authorization.topic}") + private String authorizationTopic; + @Value("${audit.kafka.action.topic}") + private String actionTopic; + @Value("${audit.kafka.file.download.topic}") + private String fileDownloadTopic; + + @Bean("auditProducerFactory") + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + configProps.put(SaslConfigs.SASL_JAAS_CONFIG, loginModule + " required username=\"" + + username + "\" password=\"" + password + "\";"); + configProps.put(SaslConfigs.SASL_MECHANISM, saslMechanism); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean("auditTemplate") + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + @Bean + public KafkaAdmin auditKafkaAdmin() { + Map configs = new HashMap<>(); + configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configs.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, securityProtocol); + configs.put(SaslConfigs.SASL_JAAS_CONFIG, loginModule + " required username=\"" + + username + "\" password=\"" + password + "\";"); + configs.put(SaslConfigs.SASL_MECHANISM, saslMechanism); + return new KafkaAdmin(configs); + } + + @Bean + public NewTopic auditAuthTopic() { + return TopicBuilder.name(authorizationTopic).build(); + } + + @Bean + public NewTopic auditActionTopic() { + return TopicBuilder.name(actionTopic).build(); + } + + @Bean + public NewTopic auditDownloadTopic() { + return TopicBuilder.name(fileDownloadTopic).build(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java new file mode 100644 index 0000000..d30e732 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java @@ -0,0 +1,43 @@ +package ru.micord.ervu.audit.constants; + + +import java.util.Map; +import java.util.Optional; + +/** + * @author Adel Kalimullin + */ +public final class AuditConstants { + public static final String SUBSYSTEM_TYPE = "FL"; + public static final String LOGOUT_EVENT_TYPE = "logout"; + public static final String LOGIN_EVENT_TYPE = "login"; + public static final String SUCCESS_STATUS = "success"; + public static final String FAILURE_STATUS = "failure"; + + private static final Map routeDescriptions = Map.of( + "/", "Главная", + "/mydata", "Мои данные", + "/subpoena", "Повестки", + "/restriction", "Временные меры" + ); + + private static final Map downloadTypes = Map.of( + "1", "Выписка из реестра воинского учета ФЛ", + "2", "Выписка из реестра повесток ФЛ" + ); + + public static String getRouteDescription(String route) { + return Optional.ofNullable(routeDescriptions.get(route)) + .orElseThrow(() -> new IllegalArgumentException("Invalid route :" + route)); + } + + public static String getDownloadType(String formatRegistry) { + return Optional.ofNullable(downloadTypes.get(formatRegistry)) + .orElseThrow( + () -> new IllegalArgumentException("Invalid formatRegistry :" + formatRegistry)); + } + + private AuditConstants() { + } + +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java new file mode 100644 index 0000000..c6e50a2 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java @@ -0,0 +1,31 @@ +package ru.micord.ervu.audit.controller; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.audit.service.AuditService; + +/** + * @author Adel Kalimullin + */ +@RestController +@RequestMapping("/audit") +public class AuditController { + private final AuditService auditService; + + public AuditController(AuditService auditService) { + this.auditService = auditService; + } + + @PostMapping("/action") + public ResponseEntity auditAction(HttpServletRequest request, + @RequestBody AuditActionRequest actionEvent) { + auditService.processActionEvent(request, actionEvent); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java new file mode 100644 index 0000000..6a2b122 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java @@ -0,0 +1,53 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class AuditActionEvent extends AuditEvent { + private String eventType; + private String description; + private String sourceUrl; + private String fileName; + + public AuditActionEvent( + String esiaPersonId, String eventTime, String eventType, + String description, String sourceUrl, String fileName) { + super(esiaPersonId, eventTime); + this.eventType = eventType; + this.description = description; + this.sourceUrl = sourceUrl; + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSourceUrl() { + return sourceUrl; + } + + public void setSourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java new file mode 100644 index 0000000..05229a7 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java @@ -0,0 +1,43 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class AuditActionRequest { + private String route; + private String sourceUrl; + private String eventType; + private String fileName; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getRoute() { + return route; + } + + public void setRoute(String route) { + this.route = route; + } + + public String getSourceUrl() { + return sourceUrl; + } + + public void setSourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java new file mode 100644 index 0000000..c6a2a14 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java @@ -0,0 +1,113 @@ +package ru.micord.ervu.audit.model; + + +public class AuditAuthorizationEvent extends AuditEvent { + private String status; + private String eventType; + private String firstName; + private String lastName; + private String middleName; + private String snils; + private String serverIp; + private String serverHostName; + private String clientIp; + private String clientHostName; + + public AuditAuthorizationEvent( + String esiaPersonId, String eventTime, String firstName, + String lastName, String middleName, String snils, + String status, String eventType, String serverIp, + String serverHostName, String clientIp, String clientHostName) { + super(esiaPersonId, eventTime); + this.status = status; + this.firstName = firstName; + this.lastName = lastName; + this.middleName = middleName; + this.snils = snils; + this.eventType = eventType; + this.serverIp = serverIp; + this.serverHostName = serverHostName; + this.clientIp = clientIp; + this.clientHostName = clientHostName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getMiddleName() { + return middleName; + } + + public void setMiddleName(String middleName) { + this.middleName = middleName; + } + + public String getSnils() { + return snils; + } + + public void setSnils(String snils) { + this.snils = snils; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public String getServerIp() { + return serverIp; + } + + public void setServerIp(String serverIp) { + this.serverIp = serverIp; + } + + public String getServerHostName() { + return serverHostName; + } + + public void setServerHostName(String serverHostName) { + this.serverHostName = serverHostName; + } + + public String getClientIp() { + return clientIp; + } + + public void setClientIp(String clientIp) { + this.clientIp = clientIp; + } + + public String getClientHostName() { + return clientHostName; + } + + public void setClientHostName(String clientHostName) { + this.clientHostName = clientHostName; + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java new file mode 100644 index 0000000..add1d73 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java @@ -0,0 +1,65 @@ +package ru.micord.ervu.audit.model; + + +/** + * @author Adel Kalimullin + */ +public class AuditDownloadEvent extends AuditEvent { + private String downloadType; + private String fileName; + private String s3FileUrl; + private String fileSize; + private String status; + + public AuditDownloadEvent( + String esiaPersonId, String eventTime, String downloadType, + String fileName, String s3FileUrl, String fileSize, + String status) { + super(esiaPersonId, eventTime); + this.downloadType = downloadType; + this.fileName = fileName; + this.s3FileUrl = s3FileUrl; + this.fileSize = fileSize; + this.status = status; + } + + public String getS3FileUrl() { + return s3FileUrl; + } + + public void setS3FileUrl(String s3FileUrl) { + this.s3FileUrl = s3FileUrl; + } + + public String getDownloadType() { + return downloadType; + } + + public void setDownloadType(String downloadType) { + this.downloadType = downloadType; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFileSize() { + return fileSize; + } + + public void setFileSize(String fileSize) { + this.fileSize = fileSize; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java new file mode 100644 index 0000000..1659a37 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java @@ -0,0 +1,38 @@ +package ru.micord.ervu.audit.model; + +import ru.micord.ervu.audit.constants.AuditConstants; + +/** + * @author Adel Kalimullin + */ + +public abstract class AuditEvent { + protected final String subsystem = AuditConstants.SUBSYSTEM_TYPE; + protected String esiaPersonId; + protected String eventTime; + + protected AuditEvent(String esiaPersonId, String eventTime) { + this.esiaPersonId = esiaPersonId; + this.eventTime = eventTime; + } + + public String getEsiaPersonId() { + return esiaPersonId; + } + + public void setEsiaPersonId(String esiaPersonId) { + this.esiaPersonId = esiaPersonId; + } + + public String getSubsystem() { + return subsystem; + } + + public String getEventTime() { + return eventTime; + } + + public void setEventTime(String eventTime) { + this.eventTime = eventTime; + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java new file mode 100644 index 0000000..bb3786f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java @@ -0,0 +1,8 @@ +package ru.micord.ervu.audit.service; + +/** + * @author Adel Kalimullin + */ +public interface AuditKafkaPublisher { + void publishEvent(String topicName, String message); +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java new file mode 100644 index 0000000..e1b7f9b --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java @@ -0,0 +1,19 @@ +package ru.micord.ervu.audit.service; + +import javax.servlet.http.HttpServletRequest; + +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.security.esia.model.PersonModel; + +/** + * @author Adel Kalimullin + */ +public interface AuditService { + void processActionEvent(HttpServletRequest request, AuditActionRequest auditActionRequest); + + void processAuthEvent(HttpServletRequest request, PersonModel personModel, String status, + String eventType); + + void processDownloadEvent(HttpServletRequest request, int fileSize, String fileName, + String formatRegistry, String status); +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java new file mode 100644 index 0000000..075c5f7 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java @@ -0,0 +1,42 @@ +package ru.micord.ervu.audit.service.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import ru.micord.ervu.audit.service.AuditKafkaPublisher; + +/** + * @author Adel Kalimullin + */ +@Service +public class BaseAuditKafkaPublisher implements AuditKafkaPublisher { + private static final Logger LOGGER = LoggerFactory.getLogger(BaseAuditKafkaPublisher.class); + private final KafkaTemplate kafkaTemplate; + @Value("${audit.kafka.enabled}") + private boolean auditEnabled; + + public BaseAuditKafkaPublisher( + @Qualifier("auditTemplate") KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void publishEvent(String topic, String message) { + if (auditEnabled) { + kafkaTemplate.send(topic, message) + .addCallback( + result -> { + }, + ex -> LOGGER.error("Failed to send message to topic {}: {}", topic, ex.getMessage(), + ex + ) + ); + } + else { + LOGGER.info("Audit is disabled. Event not published."); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java new file mode 100644 index 0000000..6dd4b91 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java @@ -0,0 +1,114 @@ +package ru.micord.ervu.audit.service.impl; + +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.model.AuditActionEvent; +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.audit.model.AuditAuthorizationEvent; +import ru.micord.ervu.audit.model.AuditDownloadEvent; +import ru.micord.ervu.audit.service.AuditKafkaPublisher; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.security.esia.model.PersonModel; +import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; +import ru.micord.ervu.util.DateUtils; +import ru.micord.ervu.util.NetworkUtils; + +/** + * @author Adel Kalimullin + */ +@Service +public class BaseAuditService implements AuditService { + private final AuditKafkaPublisher auditPublisher; + private final JwtTokenService jwtTokenService; + private final ObjectMapper objectMapper; + @Value("${audit.kafka.authorization.topic}") + private String authorizationTopic; + @Value("${audit.kafka.action.topic}") + private String actionTopic; + @Value("${audit.kafka.file.download.topic}") + private String fileDownloadTopic; + + public BaseAuditService(AuditKafkaPublisher auditPublisher, JwtTokenService jwtTokenService, + ObjectMapper objectMapper) { + this.auditPublisher = auditPublisher; + this.jwtTokenService = jwtTokenService; + this.objectMapper = objectMapper; + } + + @Override + public void processActionEvent( + HttpServletRequest request, AuditActionRequest auditActionRequest) { + String userAccountId = jwtTokenService.getUserAccountId(request); + String description = AuditConstants.getRouteDescription(auditActionRequest.getRoute()); + + AuditActionEvent event = new AuditActionEvent( + userAccountId, + DateUtils.getClientTimeFromRequest(request), + auditActionRequest.getEventType(), + description, + auditActionRequest.getSourceUrl(), + auditActionRequest.getFileName() + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(actionTopic, message); + } + + @Override + public void processAuthEvent(HttpServletRequest request, PersonModel personModel, String status, + String eventType) { + String serverIp = NetworkUtils.getServerIp(); + String clientIp = NetworkUtils.getClientIp(request); + String serverHostName = NetworkUtils.getHostName(serverIp); + String clientHostName = NetworkUtils.getHostName(clientIp); + + AuditAuthorizationEvent event = new AuditAuthorizationEvent( + personModel.getPrnsId(), + DateUtils.getClientTimeFromRequest(request), + personModel.getFirstName(), + personModel.getLastName(), + personModel.getMiddleName(), + personModel.getSnils(), + status, + eventType, + serverIp, + serverHostName, + clientIp, + clientHostName + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(authorizationTopic, message); + } + + @Override + public void processDownloadEvent( + HttpServletRequest request, int fileSize, String fileName, String formatRegistry, + String status) { + String userAccountId = jwtTokenService.getUserAccountId(request); + + AuditDownloadEvent event = new AuditDownloadEvent( + userAccountId, + DateUtils.getClientTimeFromRequest(request), + AuditConstants.getDownloadType(formatRegistry), + fileName, + null, + String.valueOf(fileSize), + status + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(fileDownloadTopic, message); + } + + private String convertToMessage(Object event) { + try { + return objectMapper.writeValueAsString(event); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java b/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java index bcce1d8..b4d3e01 100644 --- a/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java +++ b/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java @@ -2,8 +2,11 @@ package ru.micord.ervu.controller; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.common.utils.Bytes; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.InputStreamResource; @@ -14,53 +17,104 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import rtl.pgs.ervu.proto.ExtractRegistry; -import rtl.pgs.ervu.proto.ResponseData; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.dto.ExtractEmptyRequestDto; import ru.micord.ervu.dto.ExtractRequestDto; import ru.micord.ervu.exception.ProtobufParsingException; +import ru.micord.ervu.kafka.dto.EmptyExtract; +import ru.micord.ervu.kafka.dto.Extract; +import ru.micord.ervu.kafka.dto.FullExtract; import ru.micord.ervu.kafka.service.ReplyingKafkaService; +import ru.micord.ervu.security.esia.model.PersonModel; +import ru.micord.ervu.security.esia.service.PersonalDataService; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; +import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import javax.servlet.http.HttpServletRequest; + /** * @author gulnaz */ @RestController public class ExtractController { - + private final PersonalDataService personalDataService; private final ReplyingKafkaService replyingKafkaService; + private final AuditService auditService; + @Value("${ervu.kafka.registry.extract.empty.request.topic}") + private String registryExtractEmptyRequestTopic; @Value("${ervu.kafka.registry.extract.request.topic}") private String registryExtractRequestTopic; @Value("${ervu.kafka.registry.extract.reply.topic}") private String registryExtractReplyTopic; + @Value("${ervu.kafka.registry.extract.type.header:empty}") + private String registryExtractTypeHeader; - public ExtractController(ReplyingKafkaService replyingKafkaService) { + public ExtractController( + PersonalDataService personalDataService, + ReplyingKafkaService replyingKafkaService, + AuditService auditService + ) { + this.personalDataService = personalDataService; this.replyingKafkaService = replyingKafkaService; + this.auditService = auditService; } @GetMapping(value = "/extract/{formatRegistry}") - public ResponseEntity getExtract(@PathVariable String formatRegistry) { - String ervuId = SecurityUtil.getErvuId(); + public ResponseEntity getExtract(HttpServletRequest servletRequest, @PathVariable String formatRegistry) { + UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); + String ervuId = userIdsPair.getErvuId(); + ConsumerRecord record; + boolean isEmpty = true; - if (ervuId == null) { - return ResponseEntity.noContent().build(); + if (ervuId != null) { + ExtractRequestDto request = new ExtractRequestDto(ervuId, formatRegistry); + record = replyingKafkaService.sendMessageAndGetReply( + registryExtractRequestTopic, registryExtractReplyTopic, request); + isEmpty = Arrays.stream(record.headers().toArray()) + .filter(header -> header.key().equals(registryExtractTypeHeader)) + .findFirst() + .map(header -> Boolean.parseBoolean(new String(header.value(), StandardCharsets.UTF_8))) + .orElseThrow(); } - ExtractRequestDto request = new ExtractRequestDto(ervuId, formatRegistry); - byte[] reply = replyingKafkaService.sendMessageAndGetReply(registryExtractRequestTopic, - registryExtractReplyTopic, request).get(); + else { + String esiaUserId = userIdsPair.getEsiaUserId(); // esiaUserId is not null here + String esiaAccessToken = EsiaAuthInfoStore.getAccessToken(esiaUserId); + PersonModel personModel = personalDataService.getPersonModel(esiaAccessToken); + + ExtractEmptyRequestDto emptyRequest = new ExtractEmptyRequestDto( + personModel.getLastName(), + personModel.getFirstName(), personModel.getMiddleName(), personModel.getBirthDate(), + personModel.getSnils(), formatRegistry + ); + record = replyingKafkaService.sendMessageAndGetReply(registryExtractEmptyRequestTopic, + registryExtractReplyTopic, emptyRequest); + } + byte[] bytes = record.value().get(); + String fileName = null; + int size = 0; try { - ResponseData responseData = ResponseData.parseFrom(reply); - ExtractRegistry extractRegistry = responseData.getDataRegistryInformation() - .getExtractRegistry(); - String encodedFilename = URLEncoder.encode(extractRegistry.getFileName(), StandardCharsets.UTF_8); - InputStreamResource resource = new InputStreamResource(extractRegistry.getFile().newInput()); + Extract extract = ervuId == null || isEmpty ? new EmptyExtract(bytes) : new FullExtract(bytes); + fileName = extract.getFileName(); + String encodedFilename = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + ByteString file = extract.getFile(); + InputStreamResource resource = new InputStreamResource(file.newInput()); + size = file.size(); + auditService.processDownloadEvent(servletRequest, size, fileName, formatRegistry, + AuditConstants.SUCCESS_STATUS + ); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } catch (InvalidProtocolBufferException e) { + auditService.processDownloadEvent(servletRequest, size, fileName, formatRegistry, + AuditConstants.FAILURE_STATUS + ); throw new ProtobufParsingException("Failed to parse data", e); } } diff --git a/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java b/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java index a523b24..dffcec2 100644 --- a/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java +++ b/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java @@ -5,18 +5,19 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; +import org.springframework.util.StringUtils; +import proto.ervu.rp.summons.RecruitmentInfo; import ru.micord.ervu.dto.Restriction; import ru.micord.ervu.dto.SubpoenaResponseDto; import org.springframework.stereotype.Component; -import proto.ervu.rp.summons.MeasuresTemporary; import proto.ervu.rp.summons.ResponseDataAddress; import proto.ervu.rp.summons.SummonsInfo; import proto.ervu.rp.summons.SummonsResponseData; -import static ru.micord.ervu.util.DateUtil.convertToLocalDate; +import static ru.micord.ervu.util.DateUtils.convertToLocalDate; import static java.util.Objects.requireNonNull; -import static ru.micord.ervu.util.DateUtil.convertToString; -import static ru.micord.ervu.util.DateUtil.getDaysTill; +import static ru.micord.ervu.util.DateUtils.convertToString; +import static ru.micord.ervu.util.DateUtils.getDaysTill; /** * @author gulnaz @@ -29,6 +30,7 @@ public class SummonsResponseDataConverter { private static final String ACTUAL_ADDRESS_CODE = "_3"; public SubpoenaResponseDto convert(SummonsResponseData responseData) { + RecruitmentInfo recruitmentInfo = responseData.getRecruitmentInfo(); SubpoenaResponseDto.Builder builder = new SubpoenaResponseDto.Builder() .personName(responseData.getFirstName(), responseData.getMiddleName(), responseData.getLastName() @@ -40,10 +42,11 @@ public class SummonsResponseDataConverter { .issueOrg(responseData.getIssueOrg()) .issueIdCode(responseData.getIssueIdCode()) - .militaryCommissariatName(responseData.getRecruitmentInfo().getMilitaryCommissariatName()) - .recruitmentStatusCode( - Integer.parseInt(responseData.getRecruitmentInfo().getRecruitmentStatusCode())) - .recruitmentStartDate(responseData.getRecruitmentInfo().getRecruitmentStart()) + .militaryCommissariatName(recruitmentInfo.getMilitaryCommissariatName()) + .recruitmentStatusCode(StringUtils.hasText(recruitmentInfo.getRecruitmentStatusCode()) + ? Integer.parseInt(recruitmentInfo.getRecruitmentStatusCode()) + : 0) + .recruitmentStartDate(recruitmentInfo.getRecruitmentStart()) .residenceAddress(getAddressByCode(responseData.getAddressesList(), RESIDENCE_ADDRESS_CODE)) .stayAddress(getAddressByCode(responseData.getAddressesList(), STAY_ADDRESS_CODE)) diff --git a/backend/src/main/java/ru/micord/ervu/dto/ExtractEmptyRequestDto.java b/backend/src/main/java/ru/micord/ervu/dto/ExtractEmptyRequestDto.java new file mode 100644 index 0000000..03c423a --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/dto/ExtractEmptyRequestDto.java @@ -0,0 +1,8 @@ +package ru.micord.ervu.dto; + +/** + * @author r.latypov + */ +public record ExtractEmptyRequestDto(String lastName, String firstName, String middleName, + String birthDate, String snils, String formatExtractRegistry) { +} diff --git a/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java b/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java index 5d8d442..80c3a38 100644 --- a/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java +++ b/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.List; import static org.springframework.util.StringUtils.hasText; -import static ru.micord.ervu.util.DateUtil.convertToLocalDate; -import static ru.micord.ervu.util.DateUtil.convertToString; +import static ru.micord.ervu.util.DateUtils.convertToLocalDate; +import static ru.micord.ervu.util.DateUtils.convertToString; /** * @author gulnaz diff --git a/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java b/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java index e9b404a..dd7709a 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java @@ -101,6 +101,7 @@ public class ReplyingKafkaConfig { + username + "\" password=\"" + password + "\";"); configProps.put(SaslConfigs.SASL_MECHANISM, saslMechanism); configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId + "-" + UUID.randomUUID()); + configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); return configProps; } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/dto/EmptyExtract.java b/backend/src/main/java/ru/micord/ervu/kafka/dto/EmptyExtract.java new file mode 100644 index 0000000..ba0a6f9 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/dto/EmptyExtract.java @@ -0,0 +1,19 @@ +package ru.micord.ervu.kafka.dto; + +import com.google.protobuf.InvalidProtocolBufferException; +import rtl.pgs.ervu.proto.emptyrequest.ExtractRegistry; +import rtl.pgs.ervu.proto.emptyrequest.ResponseData; + +/** + * @author gulnaz + */ +public class EmptyExtract extends Extract { + + public EmptyExtract(byte[] bytes) throws InvalidProtocolBufferException { + ResponseData responseData = ResponseData.parseFrom(bytes); + ExtractRegistry extractRegistry = responseData.getDataRegistryInformation() + .getExtractRegistry(); + fileName = extractRegistry.getFileName(); + file = extractRegistry.getFile(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/dto/Extract.java b/backend/src/main/java/ru/micord/ervu/kafka/dto/Extract.java new file mode 100644 index 0000000..4f9ccdd --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/dto/Extract.java @@ -0,0 +1,20 @@ +package ru.micord.ervu.kafka.dto; + +import com.google.protobuf.ByteString; + +/** + * @author gulnaz + */ +public abstract class Extract { + + protected String fileName; + protected ByteString file; + + public String getFileName() { + return fileName; + } + + public ByteString getFile() { + return file; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/dto/FullExtract.java b/backend/src/main/java/ru/micord/ervu/kafka/dto/FullExtract.java new file mode 100644 index 0000000..83d1a4f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/dto/FullExtract.java @@ -0,0 +1,19 @@ +package ru.micord.ervu.kafka.dto; + +import com.google.protobuf.InvalidProtocolBufferException; +import rtl.pgs.ervu.proto.ExtractRegistry; +import rtl.pgs.ervu.proto.ResponseData; + +/** + * @author gulnaz + */ +public class FullExtract extends Extract { + + public FullExtract(byte[] bytes) throws InvalidProtocolBufferException { + ResponseData responseData = ResponseData.parseFrom(bytes); + ExtractRegistry extractRegistry = responseData.getDataRegistryInformation() + .getExtractRegistry(); + fileName = extractRegistry.getFileName(); + file = extractRegistry.getFile(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/exception/KafkaMessageReplyTimeoutException.java b/backend/src/main/java/ru/micord/ervu/kafka/exception/KafkaMessageReplyTimeoutException.java new file mode 100644 index 0000000..7c99bd1 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/exception/KafkaMessageReplyTimeoutException.java @@ -0,0 +1,18 @@ +package ru.micord.ervu.kafka.exception; + +import org.springframework.context.support.MessageSourceAccessor; + +import ru.cg.webbpm.modules.core.runtime.api.LocalizedException; +import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils; + +/** + * @author Emir Suleimanov + */ +public class KafkaMessageReplyTimeoutException extends LocalizedException { + private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor("messages/common_errors_messages"); + private static final String KAFKA_REPLY_TIMEOUT = "kafka_reply_timeout"; + + public KafkaMessageReplyTimeoutException(Throwable cause) { + super(KAFKA_REPLY_TIMEOUT, MESSAGE_SOURCE, cause); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/service/ReplyingKafkaService.java b/backend/src/main/java/ru/micord/ervu/kafka/service/ReplyingKafkaService.java index 3389eb5..495e3e0 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/service/ReplyingKafkaService.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/service/ReplyingKafkaService.java @@ -1,8 +1,10 @@ package ru.micord.ervu.kafka.service; +import org.apache.kafka.clients.consumer.ConsumerRecord; + public interface ReplyingKafkaService { - V sendMessageAndGetReply(String requestTopic, - String replyTopic, - T requestMessage); + ConsumerRecord sendMessageAndGetReply(String requestTopic, + String replyTopic, + T requestMessage); } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaService.java b/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaService.java index d019093..96aacea 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaService.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaService.java @@ -1,32 +1,42 @@ package ru.micord.ervu.kafka.service.impl; +import java.lang.invoke.MethodHandles; import java.util.Optional; import java.util.concurrent.ExecutionException; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.producer.ProducerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.kafka.requestreply.ReplyingKafkaTemplate; import org.springframework.kafka.requestreply.RequestReplyFuture; import ru.micord.ervu.kafka.exception.KafkaMessageException; +import ru.micord.ervu.kafka.exception.KafkaMessageReplyTimeoutException; import ru.micord.ervu.kafka.service.ReplyingKafkaService; /** * @author gulnaz */ public abstract class BaseReplyingKafkaService implements ReplyingKafkaService { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Override - public V sendMessageAndGetReply(String requestTopic, String replyTopic, T requestMessage) { + public ConsumerRecord sendMessageAndGetReply(String requestTopic, String replyTopic, T requestMessage) { + long startTime = System.currentTimeMillis(); RequestReplyFuture replyFuture = getTemplate().sendAndReceive( getProducerRecord(requestTopic, replyTopic, requestMessage)); try { - return Optional.ofNullable(replyFuture.get()) - .map(ConsumerRecord::value) + ConsumerRecord result = Optional.ofNullable(replyFuture.get()) .orElseThrow(() -> new KafkaMessageException("Kafka return result is null")); + LOGGER.info("Thread {} - KafkaSendMessageAndGetReply: {} ms", + Thread.currentThread().getId(), System.currentTimeMillis() - startTime); + return result; } catch (InterruptedException | ExecutionException e) { - throw new KafkaMessageException("Failed to get kafka response", e); + LOGGER.error("Thread {} - KafkaSendMessageAndGetReply: {} ms", + Thread.currentThread().getId(), System.currentTimeMillis() - startTime); + throw new KafkaMessageReplyTimeoutException(e); } } protected abstract ReplyingKafkaTemplate getTemplate(); diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java b/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java similarity index 71% rename from backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java rename to backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java index e7200f7..3465585 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java @@ -1,4 +1,4 @@ -package ru.micord.ervu.security.esia.token; +package ru.micord.ervu.security.esia; import java.lang.invoke.MethodHandles; import java.util.Map; @@ -6,14 +6,17 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.micord.ervu.security.esia.model.ExpiringState; +import ru.micord.ervu.security.esia.model.ExpiringToken; /** * @author Eduard Tihomirov */ -public class EsiaTokensStore { +public class EsiaAuthInfoStore { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Map ACCESS_TOKENS_MAP = new ConcurrentHashMap<>(); private static final Map REFRESH_TOKENS_MAP = new ConcurrentHashMap<>(); + private static final Map PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>(); public static void addAccessToken(String prnOid, String token, long expiresIn) { if (token != null) { @@ -75,4 +78,26 @@ public class EsiaTokensStore { public static void removeRefreshToken(String prnOid) { REFRESH_TOKENS_MAP.remove(prnOid); } + + public static void addState(String prnsUUID, String state, long expiresIn) { + long expiryTime = System.currentTimeMillis() + expiresIn * 1000L; + PRNS_UUID_STATE_MAP.put(prnsUUID, new ExpiringState(state, expiryTime)); + } + + public static String getState(String prnsUUID) { + return PRNS_UUID_STATE_MAP.get(prnsUUID).getState(); + } + + public static void removeState(String prnsUUID) { + PRNS_UUID_STATE_MAP.remove(prnsUUID); + } + + public static void removeExpiredState() { + for (String key : PRNS_UUID_STATE_MAP.keySet()) { + ExpiringState state = PRNS_UUID_STATE_MAP.get(key); + if (state != null && state.isExpired()) { + PRNS_UUID_STATE_MAP.remove(key); + } + } + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java index 8246896..242dd16 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java @@ -32,10 +32,10 @@ public class EsiaConfig { @Value("${esia.client.cert.hash}") private String clientCertHash; - @Value("${esia.request.timeout:60}") + @Value("${request.timeout:20}") private long requestTimeout; - @Value("${esia.connection.timeout:30}") + @Value("${connection.timeout:10}") private long connectionTimeout; @Value("${esia.logout.url:idp/ext/Logout}") @@ -53,6 +53,13 @@ public class EsiaConfig { @Value("${esia.issuer.url}") private String esiaIssuerUrl; + @Value("${esia.marker.ver}") + private String esiaMarkerVer; + + @Value("${esia.state.cookie.life.time:300}") + private long esiaStateCookieLifeTime; + + public String getEsiaScopes() { String[] scopeItems = esiaScopes.split(","); return String.join(" ", Arrays.stream(scopeItems).map(String::trim).toArray(String[]::new)); @@ -107,4 +114,13 @@ public class EsiaConfig { public String getEsiaIssuerUrl() { return esiaIssuerUrl; } + + public String getEsiaMarkerVer() { + return esiaMarkerVer; + } + + + public long getEsiaStateCookieLifeTime() { + return esiaStateCookieLifeTime; + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java b/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java index a0d3282..0b0b84b 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java @@ -5,10 +5,8 @@ import java.text.SimpleDateFormat; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ru.micord.ervu.security.esia.model.PersonDataModel; @@ -34,15 +32,15 @@ public class EsiaController { private JwtTokenService jwtTokenService; @RequestMapping(value = "/esia/url") - public String getEsiaUrl() { - return esiaAuthService.generateAuthCodeUrl(); + public String getEsiaUrl(HttpServletResponse response) { + return esiaAuthService.generateAuthCodeUrl(response); } @GetMapping(value = "/esia/auth") - public ResponseEntity esiaAuth(@RequestParam(value = "code", required = false) String code, - @RequestParam(value = "error", required = false) String error, HttpServletRequest request, - HttpServletResponse response) { - return esiaAuthService.getEsiaTokensByCode(code, error, request, response); + public void esiaAuth(@RequestParam String code, + @RequestParam String state, + HttpServletResponse response, HttpServletRequest request) { + esiaAuthService.authEsiaTokensByCode(code, state, response, request); } @RequestMapping(value = "/esia/refresh") diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringState.java b/backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringState.java new file mode 100644 index 0000000..7584b66 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringState.java @@ -0,0 +1,34 @@ +package ru.micord.ervu.security.esia.model; + +/** + * @author Eduard Tihomirov + */ +public class ExpiringState { + private String state; + private long expiryTime; + + public ExpiringState(String state, long expiryTime) { + this.state = state; + this.expiryTime = expiryTime; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public long getExpiryTime() { + return expiryTime; + } + + public void setExpiryTime(long expiryTime) { + this.expiryTime = expiryTime; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java b/backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringToken.java similarity index 89% rename from backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java rename to backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringToken.java index f6a476e..a20b88c 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/model/ExpiringToken.java @@ -1,4 +1,4 @@ -package ru.micord.ervu.security.esia.token; +package ru.micord.ervu.security.esia.model; /** * @author Eduard Tihomirov @@ -28,7 +28,7 @@ public class ExpiringToken { this.expiryTime = expiryTime; } - boolean isExpired() { + public boolean isExpired() { return System.currentTimeMillis() > expiryTime; } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthInfoClearShedulerService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthInfoClearShedulerService.java new file mode 100644 index 0000000..299876f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthInfoClearShedulerService.java @@ -0,0 +1,22 @@ +package ru.micord.ervu.security.esia.service; + +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; + +/** + * @author Eduard Tihomirov + */ +@Service +public class EsiaAuthInfoClearShedulerService { + @Scheduled(cron = "${esia.auth.info.clear.cron:0 0 */1 * * *}") + @SchedulerLock(name = "clearAuthInfo") + @Transactional + public void run() { + EsiaAuthInfoStore.removeExpiredRefreshToken(); + EsiaAuthInfoStore.removeExpiredAccessToken(); + EsiaAuthInfoStore.removeExpiredState(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index f3c048b..076100e 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -9,22 +9,30 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; +import java.util.UUID; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.util.WebUtils; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.kafka.model.Document; import ru.micord.ervu.kafka.model.Person; import ru.micord.ervu.kafka.model.Response; @@ -35,8 +43,7 @@ import ru.micord.ervu.security.esia.model.EsiaHeader; import ru.micord.ervu.security.esia.model.EsiaTokenResponse; import ru.micord.ervu.security.esia.model.FormUrlencoded; import ru.micord.ervu.security.esia.model.PersonModel; -import ru.micord.ervu.security.esia.model.SignResponse; -import ru.micord.ervu.security.esia.token.EsiaTokensStore; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; import ru.micord.ervu.security.esia.config.EsiaConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -48,7 +55,10 @@ import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; import ru.micord.ervu.security.webbpm.jwt.model.Token; -import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUsername; +import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils; + +import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUserEsiaId; +import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils; /** * @author Eduard Tihomirov @@ -56,7 +66,9 @@ import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUse @Service public class EsiaAuthService { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - + private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor( + "messages/common_errors_messages"); + private static final String PRNS_UUID = "prns_uuid"; @Autowired private ObjectMapper objectMapper; @@ -73,6 +85,9 @@ public class EsiaAuthService { @Autowired private PersonalDataService personalDataService; + @Autowired + private AuditService auditService; + @Autowired private SecurityHelper securityHelper; @@ -82,12 +97,14 @@ public class EsiaAuthService { @Value("${ervu.kafka.request.topic}") private String requestTopic; - public String generateAuthCodeUrl() { + public String generateAuthCodeUrl(HttpServletResponse response) { try { String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); + String state = UUID.randomUUID().toString(); + String prnsUUID = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A") .replaceAll("/", "%2F"); @@ -97,12 +114,15 @@ public class EsiaAuthService { parameters.put("client_id", clientId); parameters.put("scope", scope); parameters.put("timestamp", timestamp); - parameters.put("state", "%s"); + parameters.put("state", state); parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); - SignResponse signResponse = signMap(parameters); - String state = signResponse.getState(); - String clientSecret = signResponse.getSignature(); + String clientSecret = signMap(parameters); + EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime()); + ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/") + .maxAge(esiaConfig.getEsiaStateCookieLifeTime()) + .build(); + securityHelper.addResponseCookie(response, prnsCookie); String responseType = "code"; @@ -151,22 +171,21 @@ public class EsiaAuthService { return uriBuilder.toString(); } - public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, String error, - HttpServletRequest request, HttpServletResponse response) { - if (error != null && !error.equals("null")) { - return new ResponseEntity<>( - "Произошла неизвестная ошибка. Обратитесь к системному администратору", - HttpStatus.FORBIDDEN - ); - } + public void authEsiaTokensByCode(String esiaAuthCode, String state, HttpServletResponse response, HttpServletRequest request) { String esiaAccessTokenStr = null; String prnOid = null; Long expiresIn = null; + long signSecret = 0, requestAccessToken = 0, verifySecret = 0; + String verifyStateResult = verifyStateFromCookie(request, state, response); + if (verifyStateResult != null) { + throw new EsiaException(verifyStateResult); + } try { String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); + String newState = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String scope = esiaConfig.getEsiaScopes(); @@ -174,26 +193,27 @@ public class EsiaAuthService { parameters.put("client_id", clientId); parameters.put("scope", scope); parameters.put("timestamp", timestamp); - parameters.put("state", "%s"); + parameters.put("state", newState); parameters.put("redirect_uri", redirectUrl); parameters.put("code", esiaAuthCode); - SignResponse signResponse = signMap(parameters); - String state = signResponse.getState(); - String clientSecret = signResponse.getSignature(); + long startTime = System.currentTimeMillis(); + String clientSecret = signMap(parameters); + signSecret = System.currentTimeMillis() - startTime; String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl(); String postBody = new FormUrlencoded() .setParameter("client_id", clientId) .setParameter("code", esiaAuthCode) .setParameter("grant_type", "authorization_code") .setParameter("client_secret", clientSecret) - .setParameter("state", state) + .setParameter("state", newState) .setParameter("redirect_uri", redirectUrl) .setParameter("scope", scope) .setParameter("timestamp", timestamp) .setParameter("token_type", "Bearer") .setParameter("client_certificate_hash", esiaConfig.getClientCertHash()) .toFormUrlencodedString(); + startTime = System.currentTimeMillis(); HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl)) .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(postBody)) @@ -203,6 +223,7 @@ public class EsiaAuthService { .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout())) .build() .send(postReq, HttpResponse.BodyHandlers.ofString()); + requestAccessToken = System.currentTimeMillis() - startTime; String responseString = postResp.body(); EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class @@ -213,8 +234,13 @@ public class EsiaAuthService { tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty"; throw new IllegalStateException("Esia response error. " + errMsg); } + if (!tokenResponse.getState().equals(newState)) { + throw new EsiaException("Token invalid. State from request not equals with state from response."); + } esiaAccessTokenStr = tokenResponse.getAccessToken(); + startTime = System.currentTimeMillis(); String verifyResult = verifyToken(esiaAccessTokenStr); + verifySecret = System.currentTimeMillis() - startTime; if (verifyResult != null) { throw new EsiaException(verifyResult); } @@ -222,26 +248,37 @@ public class EsiaAuthService { EsiaAccessToken esiaAccessToken = personalDataService.readToken(esiaAccessTokenStr); prnOid = esiaAccessToken.getSbjId(); expiresIn = tokenResponse.getExpiresIn(); - EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); - EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); + EsiaAuthInfoStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); + EsiaAuthInfoStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); } catch (Exception e) { throw new EsiaException(e); } + finally { + LOGGER.info("Thread {} - SignSecret: {} ms RequestAccessToken: {} ms VerifySecret: {} ms", + Thread.currentThread().getId(), signSecret, requestAccessToken, verifySecret); + } + PersonModel personModel = null; + String ervuId = null, status = null; try { - Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); - createTokenAndAddCookie(response, prnOid, ervuIdResponse.getErvuId(), expiresIn); - return ResponseEntity.ok("Authentication successful"); + personModel = personalDataService.getPersonModel(esiaAccessTokenStr); + Response ervuIdResponse = getErvuIdResponse(personModel); + ervuId = ervuIdResponse.getErvuId(); + status = AuditConstants.SUCCESS_STATUS; } catch (Exception e) { - createTokenAndAddCookie(response, prnOid, null, expiresIn); - String messageId = getMessageId(e); - String messageWithId = String.format("[%s] %s", messageId, e.getMessage()); - LOGGER.error(messageWithId, e); - return new ResponseEntity<>( - "Произошла ошибка " + messageId + ". Обратитесь к системному администратору", - HttpStatus.FORBIDDEN - ); + status = AuditConstants.FAILURE_STATUS; + if (e instanceof EsiaException || e instanceof JsonProcessingException) { + throw new EsiaException(e); + } + } + finally { + if (personModel != null) { + auditService.processAuthEvent( + request, personModel, status, AuditConstants.LOGIN_EVENT_TYPE + ); + } + createTokenAndAddCookie(response, prnOid, ervuId, expiresIn); } } @@ -252,19 +289,18 @@ public class EsiaAuthService { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); + String state = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); Map parameters = new LinkedHashMap(); parameters.put("client_id", clientId); parameters.put("scope", esiaConfig.getEsiaScopes()); parameters.put("timestamp", timestamp); - parameters.put("state", "%s"); + parameters.put("state", state); parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); parameters.put("refresh_token", refreshToken); - SignResponse signResponse = signMap(parameters); - String state = signResponse.getState(); - String clientSecret = signResponse.getSignature(); + String clientSecret = signMap(parameters); String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl(); String postBody = new FormUrlencoded() .setParameter("client_id", clientId) @@ -303,9 +339,10 @@ public class EsiaAuthService { EsiaAccessToken esiaAccessToken = personalDataService.readToken(esiaAccessTokenStr); String prnOid = esiaAccessToken.getSbjId(); Long expiresIn = tokenResponse.getExpiresIn(); - EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); - EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshTokenStr, expiresIn); - Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); + EsiaAuthInfoStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); + EsiaAuthInfoStore.addRefreshToken(prnOid, esiaNewRefreshTokenStr, expiresIn); + PersonModel personModel = personalDataService.getPersonModel(esiaAccessTokenStr); + Response ervuIdResponse = getErvuIdResponse(personModel); createTokenAndAddCookie(response, esiaAccessToken.getSbjId(), ervuIdResponse.getErvuId(), expiresIn); } catch (Exception e) { @@ -313,7 +350,7 @@ public class EsiaAuthService { } } - private SignResponse signMap(Map paramsToSign) { + private String signMap(Map paramsToSign) { try { StringBuilder toSign = new StringBuilder(); for (String s : paramsToSign.values()) { @@ -325,13 +362,14 @@ public class EsiaAuthService { .uri(URI.create(esiaConfig.getSignUrl())) .header("Content-Type", "text/plain") .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) + .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout())) .build(); HttpResponse response = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout())) .build() .send(request, HttpResponse.BodyHandlers.ofString()); errorHandler(response); - return objectMapper.readValue(response.body(), SignResponse.class); + return response.body(); } catch (Exception e) { @@ -346,36 +384,42 @@ public class EsiaAuthService { } public String logout(HttpServletRequest request, HttpServletResponse response) { + PersonModel personModel = null; try { - securityHelper.clearAccessCookies(response); String userId = jwtTokenService.getUserAccountId(request); - EsiaTokensStore.removeAccessToken(userId); - EsiaTokensStore.removeRefreshToken(userId); + String accessToken = EsiaAuthInfoStore.getAccessToken(userId); + personModel = personalDataService.getPersonModel(accessToken); + securityHelper.clearAccessCookies(response); + EsiaAuthInfoStore.removeAccessToken(userId); + EsiaAuthInfoStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); String redirectUrl = esiaConfig.getLogoutRedirectUrl(); URL url = new URL(logoutUrl); Map params = mapOf( "client_id", esiaConfig.getClientId(), - "redirect_url", redirectUrl); + "redirect_url", redirectUrl + ); + auditService.processAuthEvent( + request, personModel, AuditConstants.SUCCESS_STATUS, AuditConstants.LOGOUT_EVENT_TYPE + ); return buildUrl(url, params); } catch (Exception e) { + if (personModel != null){ + auditService.processAuthEvent( + request, personModel, AuditConstants.FAILURE_STATUS, AuditConstants.LOGOUT_EVENT_TYPE + ); + } throw new EsiaException(e); } } - public Response getErvuIdResponse(String accessToken) { - try { - PersonModel personModel = personalDataService.getPersonModel(accessToken); - Person person = copyToPerson(personModel); - String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, - requestReplyTopic, objectMapper.writeValueAsString(person) - ); - return objectMapper.readValue(kafkaResponse, Response.class); - } - catch (Exception e) { - throw new EsiaException(e); - } + public Response getErvuIdResponse(PersonModel personModel) throws JsonProcessingException { + Person person = copyToPerson(personModel); + String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, + requestReplyTopic, objectMapper.writeValueAsString(person) + ).value(); + return objectMapper.readValue(kafkaResponse, Response.class); } private Person copyToPerson(PersonModel personModel) { @@ -393,13 +437,6 @@ public class EsiaAuthService { return person; } - private String getMessageId(Exception exception) { - return Integer.toUnsignedString(Objects - .hashCode(getCurrentUsername()), 36) - + "-" - + Integer.toUnsignedString(exception.hashCode(), 36); - } - private void createTokenAndAddCookie(HttpServletResponse response, String userId, String ervuId, Long expiresIn) { Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId); @@ -419,6 +456,9 @@ public class EsiaAuthService { if (!esiaHeader.getSbt().equals("access")) { return "Token invalid. Token sbt: " + esiaHeader.getSbt() + " invalid"; } + if (!esiaHeader.getVer().equals(esiaConfig.getEsiaMarkerVer())) { + return "Token invalid. Token ver: " + esiaHeader.getVer() + " invalid"; + } if (!esiaHeader.getTyp().equals("JWT")) { return "Token invalid. Token type: " + esiaHeader.getTyp() + " invalid"; } @@ -428,17 +468,16 @@ public class EsiaAuthService { if (!esiaAccessToken.getIss().equals(esiaConfig.getEsiaIssuerUrl())) { return "Token invalid. Token issuer:" + esiaAccessToken.getIss() + " invalid"; } - //TODO SUPPORT-8750 -// LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()), -// ZoneId.systemDefault() -// ); -// LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()), -// ZoneId.systemDefault() -// ); -// LocalDateTime currentTime = LocalDateTime.now(); -// if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) { -// return "Token invalid. Token expired"; -// } + LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()), + ZoneId.systemDefault() + ); + LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()), + ZoneId.systemDefault() + ); + LocalDateTime currentTime = LocalDateTime.now(); + if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) { + return "Token invalid. Token expired"; + } HttpResponse response = signVerify(accessToken); if (response.statusCode() != 200) { if (response.statusCode() == 401) { @@ -455,6 +494,7 @@ public class EsiaAuthService { .uri(URI.create(esiaConfig.getSignVerifyUrl())) .header("Content-Type", "text/plain") .POST(HttpRequest.BodyPublishers.ofString(accessToken, StandardCharsets.UTF_8)) + .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout())) .build(); return HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout())) @@ -465,4 +505,19 @@ public class EsiaAuthService { throw new EsiaException(e); } } + + private String verifyStateFromCookie(HttpServletRequest request, String state, HttpServletResponse response) { + Cookie cookie = WebUtils.getCookie(request, PRNS_UUID); + if (cookie == null) { + return "State invalid. Cookie not found"; + } + String prnsUUID = cookie.getValue(); + String oldState = EsiaAuthInfoStore.getState(prnsUUID); + if (oldState == null || !oldState.equals(state)) { + return "State invalid. State from ESIA not equals with state before"; + } + EsiaAuthInfoStore.removeState(prnsUUID); + securityHelper.clearCookie(response, PRNS_UUID, "/"); + return null; + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java index 49f2450..01497ca 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java @@ -1,5 +1,6 @@ package ru.micord.ervu.security.esia.service; +import java.lang.invoke.MethodHandles; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -8,6 +9,8 @@ import java.time.Duration; import java.util.Base64; import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ru.micord.ervu.security.esia.exception.EsiaException; import ru.micord.ervu.security.esia.config.EsiaConfig; import ru.micord.ervu.security.esia.model.EsiaAccessToken; @@ -23,6 +26,7 @@ import org.springframework.stereotype.Service; */ @Service public class EsiaPersonalDataService implements PersonalDataService { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Autowired private EsiaConfig esiaConfig; @@ -32,6 +36,7 @@ public class EsiaPersonalDataService implements PersonalDataService { @Override public PersonModel getPersonModel(String accessToken) { + long startTime = System.currentTimeMillis(); try { EsiaAccessToken esiaAccessToken = readToken(accessToken); String prnsId = esiaAccessToken.getSbjId(); @@ -39,9 +44,11 @@ public class EsiaPersonalDataService implements PersonalDataService { personModel.setPassportModel( getPassportModel(prnsId, accessToken, personModel.getrIdDoc())); personModel.setPrnsId(prnsId); + LOGGER.info("Thread {} - RequestPersonData: {} ms", Thread.currentThread().getId(), System.currentTimeMillis() - startTime); return personModel; } catch (Exception e) { + LOGGER.error("Thread {} - RequestPersonData: {} ms", Thread.currentThread().getId(), System.currentTimeMillis() - startTime); throw new EsiaException(e); } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java deleted file mode 100644 index 89c9db4..0000000 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.micord.ervu.security.esia.token; - -import net.javacrumbs.shedlock.core.SchedulerLock; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Eduard Tihomirov - */ -@Service -public class TokensClearShedulerService { - @Scheduled(cron = "${esia.token.clear.cron:0 0 */1 * * *}") - @SchedulerLock(name = "clearToken") - @Transactional - public void load() { - EsiaTokensStore.removeExpiredRefreshToken(); - EsiaTokensStore.removeExpiredAccessToken(); - } -} diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthentication.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthentication.java index 47f6567..bc6aa03 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthentication.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthentication.java @@ -17,8 +17,11 @@ public class JwtAuthentication implements Authentication { private final Authentication authentication; private final String token; + private final UserIdsPair userIdsPair; + public JwtAuthentication(Authentication authentication, String userAccountId, String token) { this.userAccountId = userAccountId; + this.userIdsPair = new UserIdsPair(userAccountId); this.authentication = authentication; this.token = token; } @@ -31,6 +34,10 @@ public class JwtAuthentication implements Authentication { return userAccountId; } + public UserIdsPair getUserIdsPair() { + return userIdsPair; + } + @Override public Collection getAuthorities() { return authentication.getAuthorities(); diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/UserIdsPair.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/UserIdsPair.java new file mode 100644 index 0000000..e28f8f2 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/UserIdsPair.java @@ -0,0 +1,36 @@ +package ru.micord.ervu.security.webbpm.jwt; + +public class UserIdsPair { + private final String esiaUserId; + private final String ervuId; + + public UserIdsPair(String idsConcatenated) { + + if (idsConcatenated == null) { + this.esiaUserId = null; + this.ervuId = null; + } + else { + String[] ids = idsConcatenated.split(":"); + this.esiaUserId = ids[0]; + this.ervuId = ids.length == 2 ? ids[1] : null; + } + } + + public UserIdsPair(String esiaUserId, String ervuId) { + this.esiaUserId = esiaUserId; + this.ervuId = ervuId; + } + + public String getEsiaUserId() { + return esiaUserId; + } + + public String getErvuId() { + return ervuId; + } + + public String getIdsConcatenated() { + return esiaUserId + (ervuId == null ? "" : ":" + ervuId); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java index 9c966f6..3f002db 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java @@ -48,7 +48,13 @@ public final class SecurityHelper { addResponseCookie(response, emptyAuthMarker); } - private void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { + public void clearCookie(HttpServletResponse response, String name, String path) { + ResponseCookie emptyCookie = createCookie(name, null, path) + .maxAge(0).build(); + addResponseCookie(response, emptyCookie); + } + + public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java index dfb84b9..f748ac9 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java @@ -14,8 +14,9 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import ru.micord.ervu.security.esia.token.EsiaTokensStore; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; import ru.micord.ervu.security.exception.UnauthorizedException; +import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.cg.webbpm.modules.resources.api.ResourceMetadataUtils; @@ -43,16 +44,17 @@ public class JwtTokenService { } public Token createAccessToken(String userAccountId, Long expiresIn, String ervuId) { + String idsConcatenated = new UserIdsPair(userAccountId, ervuId).getIdsConcatenated(); Date expirationDate = new Date(System.currentTimeMillis() + 1000L * expiresIn); String value = Jwts.builder() - .setSubject(userAccountId + ":" + ervuId) + .setSubject(idsConcatenated) .setIssuer(tokenIssuerName) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(expirationDate) .signWith(signingKey) .compact(); - return new Token(userAccountId + ":" + ervuId, tokenIssuerName, expirationDate, value); + return new Token(idsConcatenated, tokenIssuerName, expirationDate, value); } public boolean isValid(Token token) { @@ -65,8 +67,8 @@ public class JwtTokenService { LOGGER.info("Token {} is expired ", token.getValue()); return false; } - String[] ids = token.getUserAccountId().split(":"); - return EsiaTokensStore.validateAccessToken(ids[0]); + String esiaUserId = new UserIdsPair(token.getUserAccountId()).getEsiaUserId(); + return EsiaAuthInfoStore.validateAccessToken(esiaUserId); } public Token getToken(String token) { @@ -79,19 +81,19 @@ public class JwtTokenService { } public String getAccessToken(HttpServletRequest request) { - return EsiaTokensStore.getAccessToken(getUserAccountId(request)); + return EsiaAuthInfoStore.getAccessToken(getUserAccountId(request)); } public String getRefreshToken(HttpServletRequest request) { - return EsiaTokensStore.getRefreshToken(getUserAccountId(request)); + return EsiaAuthInfoStore.getRefreshToken(getUserAccountId(request)); } public String getUserAccountId(HttpServletRequest request) { String authToken = extractAuthToken(request); if (authToken != null) { - String[] ids = getToken(authToken).getUserAccountId().split(":"); - return ids[0]; + String esiaUserId = new UserIdsPair(getToken(authToken).getUserAccountId()).getEsiaUserId(); + return esiaUserId; } else { throw new UnauthorizedException("Failed to get auth data. User unauthorized."); diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java index ef8555e..d1dcafe 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java @@ -8,6 +8,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.util.WebUtils; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; +import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; public final class SecurityUtil { public static final String AUTH_TOKEN = "auth_token"; @@ -23,17 +24,13 @@ public final class SecurityUtil { return cookie != null ? cookie.getValue() : null; } - public static String getErvuId() { + public static UserIdsPair getUserIdsPair() { return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .map(a -> ((JwtAuthentication) a).getUserAccountId()) - .map(userAccountId -> { - String ervuId = userAccountId.split(":")[1]; - return "null".equals(ervuId) ? null : ervuId; - }) + .map(a -> ((JwtAuthentication) a).getUserIdsPair()) .orElse(null); } - public static String getCurrentUsername() { + public static String getCurrentUserEsiaId() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { return auth.getName(); diff --git a/backend/src/main/java/ru/micord/ervu/service/SubpoenaService.java b/backend/src/main/java/ru/micord/ervu/service/SubpoenaService.java index 438338d..b5ff0cd 100644 --- a/backend/src/main/java/ru/micord/ervu/service/SubpoenaService.java +++ b/backend/src/main/java/ru/micord/ervu/service/SubpoenaService.java @@ -31,14 +31,16 @@ public class SubpoenaService { } public SubpoenaResponseDto getSubpoenaData() { - String ervuId = SecurityUtil.getErvuId(); + String ervuId = SecurityUtil.getUserIdsPair() == null + ? null + : SecurityUtil.getUserIdsPair().getErvuId(); if (ervuId == null) { return new SubpoenaResponseDto.Builder().build(); } SubpoenaRequestDto subpoenaRequestDto = new SubpoenaRequestDto(ervuId); byte[] reply = replyingKafkaService.sendMessageAndGetReply(recruitRequestTopic, - recruitReplyTopic, subpoenaRequestDto).get(); + recruitReplyTopic, subpoenaRequestDto).value().get(); try { SummonsResponseData responseData = SummonsResponseData.parseFrom(reply); diff --git a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java similarity index 57% rename from backend/src/main/java/ru/micord/ervu/util/DateUtil.java rename to backend/src/main/java/ru/micord/ervu/util/DateUtils.java index 232bae5..614df49 100644 --- a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java +++ b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java @@ -1,19 +1,39 @@ package ru.micord.ervu.util; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import org.springframework.util.StringUtils; +import javax.servlet.http.HttpServletRequest; + /** * @author gulnaz */ -public final class DateUtil { +public final class DateUtils { public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); - private DateUtil() {} + private static final DateTimeFormatter DATE_TIME_WITH_TIMEZONE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + + private DateUtils() { + } + + public static String getClientTimeFromRequest(HttpServletRequest request) { + String clientTimeZone = request.getHeader("Client-Time-Zone"); + ZoneId zoneId; + try { + zoneId = ZoneId.of(clientTimeZone); + } + catch (Exception e) { + zoneId = ZoneId.systemDefault(); + } + return ZonedDateTime.now(zoneId).format(DATE_TIME_WITH_TIMEZONE_FORMATTER); + } public static LocalDate convertToLocalDate(String date) { return StringUtils.hasText(date) diff --git a/backend/src/main/java/ru/micord/ervu/util/NetworkUtils.java b/backend/src/main/java/ru/micord/ervu/util/NetworkUtils.java new file mode 100644 index 0000000..c4965cf --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/util/NetworkUtils.java @@ -0,0 +1,53 @@ +package ru.micord.ervu.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +/** + * @author Adel Kalimullin + */ +public final class NetworkUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(NetworkUtils.class); + private static final String IP_HEADER = "X-Forwarded-For"; + private static final String UNKNOWN = "unknown"; + + private NetworkUtils() { + } + + public static String getServerIp() { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + return inetAddress.getHostAddress(); + } + catch (UnknownHostException e) { + LOGGER.error("Failed to get local IP address", e); + return UNKNOWN; + } + } + + public static String getClientIp(HttpServletRequest request) { + String ip = request.getHeader(IP_HEADER); + if (StringUtils.hasText(ip) && !ip.equalsIgnoreCase(UNKNOWN)) { + return ip.split(",")[0].trim(); + } + else { + return request.getRemoteAddr(); + } + } + + public static String getHostName(String ip) { + try { + InetAddress inetAddress = InetAddress.getByName(ip); + return inetAddress.getHostName(); + } + catch (UnknownHostException e) { + LOGGER.error("Unknown host for IP {}", ip, e); + return UNKNOWN; + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/ProtoResponseUnknownRecruitData.proto b/backend/src/main/resources/ProtoResponseUnknownRecruitData.proto new file mode 100644 index 0000000..807530a --- /dev/null +++ b/backend/src/main/resources/ProtoResponseUnknownRecruitData.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package rtl.pgs.ervu.proto.emptyrequest; +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option java_outer_classname = "LkrpUnknownRecruitResponse"; +option java_package = "rtl.pgs.ervu.proto.emptyrequest"; + +message ExtractRegistry { + string fileName = 1; + string fileType = 2; + string fileDatetime = 3; + bytes file = 4; +}; + +message DataRegistryInformation { + ExtractRegistry extractRegistry = 1; +}; + +message ResponseData { + string lastName = 1; + string firstName = 2; + string middleName = 3; + string birthDate = 4; + DataRegistryInformation dataRegistryInformation = 5; +}; \ No newline at end of file diff --git a/backend/src/main/resources/i18n/exception_handler_messages.properties b/backend/src/main/resources/i18n/exception_handler_messages.properties new file mode 100644 index 0000000..d7091e8 --- /dev/null +++ b/backend/src/main/resources/i18n/exception_handler_messages.properties @@ -0,0 +1 @@ +error.unknown=Система временно недоступна. Пожалуйста, повторите попытку позже. diff --git a/backend/src/main/resources/i18n/exception_handler_messages_en_US.properties b/backend/src/main/resources/i18n/exception_handler_messages_en_US.properties new file mode 100644 index 0000000..878998f --- /dev/null +++ b/backend/src/main/resources/i18n/exception_handler_messages_en_US.properties @@ -0,0 +1 @@ +error.unknown=The system is temporarily unavailable. Please try again later. diff --git a/backend/src/main/resources/messages/common_errors_messages.properties b/backend/src/main/resources/messages/common_errors_messages.properties new file mode 100644 index 0000000..67508e9 --- /dev/null +++ b/backend/src/main/resources/messages/common_errors_messages.properties @@ -0,0 +1,2 @@ +kafka_reply_timeout=Превышено время ожидания ответа от сервера. Попробуйте повторить запрос позже или обратитесь к системному администратору +access_denied=Доступ запрещен. Пользователь должен быть включен в группу "Сотрудник, ответственный за военно-учетную работу" в ЕСИА \ No newline at end of file diff --git a/backend/src/main/resources/messages/common_errors_messages_ru_RU.properties b/backend/src/main/resources/messages/common_errors_messages_ru_RU.properties new file mode 100644 index 0000000..67508e9 --- /dev/null +++ b/backend/src/main/resources/messages/common_errors_messages_ru_RU.properties @@ -0,0 +1,2 @@ +kafka_reply_timeout=Превышено время ожидания ответа от сервера. Попробуйте повторить запрос позже или обратитесь к системному администратору +access_denied=Доступ запрещен. Пользователь должен быть включен в группу "Сотрудник, ответственный за военно-учетную работу" в ЕСИА \ No newline at end of file diff --git a/config.md b/config.md index cda83d1..52b3332 100644 --- a/config.md +++ b/config.md @@ -784,9 +784,20 @@ JBPM использует 3 корневых категории логирова - `ERVU_KAFKA_RECRUIT_HEADER_CLASS` - класс для идентификации в заголовке запроса на получение данных о повестке, временных мерах и воинском учете - `ERVU_KAFKA_SUBPOENA_EXTRACT_REQUEST_TOPIC` - топик для отправки запроса на получение выписки из Реестра повесток - `ERVU_KAFKA_SUBPOENA_EXTRACT_REPLY_TOPIC` - топик для получения выписки из Реестра повесток -- `ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC` - топик для отправки запроса на получение выписки из Реестра воинского учета +- `ERVU_KAFKA_REGISTRY_EXTRACT_EMPTY_REQUEST_TOPIC` - топик для отправки запроса на получение выписки из Реестра воинского учета при отсутствии ErvuId +- `ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC` - топик для отправки запроса на получение выписки из Реестра воинского учета при наличии ErvuId - `ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC` - топик для получения выписки из Реестра воинского учета - `ERVU_KAFKA_EXTRACT_HEADER_CLASS` - класс для идентификации в заголовке запроса на получение выписки из Реестра повесток/Реестра воинского учета +- `AUDIT_KAFKA_AUTHORIZATION_TOPIC` - топик для отправки аудита в журнал авторизации +- `AUDIT_KAFKA_ACTION_TOPIC` - топик для отправки аудита в журнал действий пользователя +- `AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC` - топик для отправки аудита в журнал загрузки ЮЛ и ФЛ +- `AUDIT_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka +- `AUDIT_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами +- `AUDIT_KAFKA_DOC_LOGIN_MODULE` - имя класса для входа в систему для SASL-соединений в формате, используемом конфигурационными файлами JAAS +- `AUDIT_KAFKA_USERNAME` - пользователь для подключения к Kafka +- `AUDIT_KAFKA_PASSWORD` - пароль для подключения к Kafka +- `AUDIT_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений +- `AUDIT_KAFKA_ENABLED` - флажок для включения записи аудита в кафку # Прочее diff --git a/config/Dockerfile b/config/Dockerfile index 32531a5..ba83ee6 100644 --- a/config/Dockerfile +++ b/config/Dockerfile @@ -1,6 +1,6 @@ ARG BUILDER_IMAGE=registry.altlinux.org/basealt/altsp:c10f1 ARG BACKEND_IMAGE=repo.micord.ru/alt/alt-tomcat:c10f1-9.0.59-20240903 -ARG FRONTEND_IMAGE=nginx:1.24-alpine-slim +ARG FRONTEND_IMAGE=nginx:1.26.2-alpine-slim FROM $BUILDER_IMAGE AS builder diff --git a/config/Dockerfile.frontend b/config/Dockerfile.frontend index 8011098..31baf91 100644 --- a/config/Dockerfile.frontend +++ b/config/Dockerfile.frontend @@ -1,5 +1,5 @@ ARG BUILDER_IMAGE=registry.altlinux.org/basealt/altsp:c10f1 -ARG RUNTIME_IMAGE=nginx:1.24-alpine-slim +ARG RUNTIME_IMAGE=nginx:1.26.2-alpine-slim FROM $BUILDER_IMAGE AS builder diff --git a/config/local.env b/config/local.env index 295ca5d..ab09c06 100644 --- a/config/local.env +++ b/config/local.env @@ -30,9 +30,10 @@ ERVU_KAFKA_REPLY_TIMEOUT=5 ERVU_KAFKA_RECRUIT_REQUEST_TOPIC=ervu.recruit.info.request ERVU_KAFKA_RECRUIT_REPLY_TOPIC=ervu.recruit.info.response ERVU_KAFKA_RECRUIT_HEADER_CLASS=Request@urn://rostelekom.ru/RP-SummonsTR/1.0.5 +ERVU_KAFKA_REGISTRY_EXTRACT_EMPTY_REQUEST_TOPIC=ervu.extract.empty.request ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC=ervu.extract.info.request ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC=ervu.extract.info.response ERVU_KAFKA_EXTRACT_HEADER_CLASS=request@urn://rostelekom.ru/ERVU-extractFromRegistryTR/1.0.3 -ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * +ESIA_AUTH_INFO_CLEAR_CRON=0 0 */1 * * * COOKIE_PATH=/fl diff --git a/config/micord.env b/config/micord.env index 74ef43f..c97462f 100644 --- a/config/micord.env +++ b/config/micord.env @@ -30,10 +30,21 @@ ERVU_KAFKA_REPLY_TIMEOUT=30 ERVU_KAFKA_RECRUIT_REQUEST_TOPIC=ervu.recruit.info.request ERVU_KAFKA_RECRUIT_REPLY_TOPIC=ervu.recruit.info.response ERVU_KAFKA_RECRUIT_HEADER_CLASS=Request@urn://rostelekom.ru/RP-SummonsTR/1.0.5 +ERVU_KAFKA_REGISTRY_EXTRACT_EMPTY_REQUEST_TOPIC=ervu.extract.empty.request ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC=ervu.extract.info.request ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC=ervu.extract.info.response ERVU_KAFKA_EXTRACT_HEADER_CLASS=request@urn://rostelekom.ru/ERVU-extractFromRegistryTR/1.0.3 KAFKA_AUTH_SASL_MODULE=org.apache.kafka.common.security.scram.ScramLoginModule +AUDIT_KAFKA_AUTHORIZATION_TOPIC=ervu.lkrp.auth.events +AUDIT_KAFKA_ACTION_TOPIC=ervu.lkrp.action.events +AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC=ervu.lkrp.import.file +AUDIT_KAFKA_BOOTSTRAP_SERVERS= +AUDIT_KAFKA_SECURITY_PROTOCOL= +AUDIT_KAFKA_DOC_LOGIN_MODULE= +AUDIT_KAFKA_USERNAME= +AUDIT_KAFKA_PASSWORD= +AUDIT_KAFKA_SASL_MECHANISM= +AUDIT_KAFKA_ENABLED=false -ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * +ESIA_AUTH_INFO_CLEAR_CRON=0 0 */1 * * * COOKIE_PATH=/fl diff --git a/config/nginx.conf b/config/nginx.conf index 85b7e6a..511895d 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -81,7 +81,7 @@ http { index index.html; try_files $uri @index; - add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self'; style-src 'unsafe-inline' 'self' data:; font-src 'self' data:; img-src 'self' data:;"; + add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; connect-src 'self' https://www.sberbank.ru; script-src 'self'; style-src 'unsafe-inline' 'self' data:; font-src 'self' data:; img-src 'self' data:;"; #Application config location = /src/resources/app-config.json { @@ -106,7 +106,7 @@ http { location @index { root /frontend; add_header Cache-Control "no-cache"; - add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self'; style-src 'unsafe-inline' 'self' data:; font-src 'self' data:; img-src 'self' data:;"; + add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; connect-src 'self' https://www.sberbank.ru; script-src 'self'; style-src 'unsafe-inline' 'self' data:; font-src 'self' data:; img-src 'self' data:;"; expires 0; try_files /index.html =404; } diff --git a/config/standalone/dev/standalone.xml b/config/standalone/dev/standalone.xml index ca7c9b3..53d67c4 100644 --- a/config/standalone/dev/standalone.xml +++ b/config/standalone/dev/standalone.xml @@ -76,11 +76,22 @@ + - + + + + + + + + + + + diff --git a/config/tomcat/etc/tomcat/server.xml b/config/tomcat/etc/tomcat/server.xml index ec52cfa..af21aff 100644 --- a/config/tomcat/etc/tomcat/server.xml +++ b/config/tomcat/etc/tomcat/server.xml @@ -162,8 +162,8 @@ Documentation at: /docs/config/valve.html Note: The pattern used is equivalent to using pattern="common" --> + prefix="localhost_access_log" suffix=".log" + pattern="%h %l %u %t "%r" %s %b %D" /> diff --git a/frontend/browser_check.js b/frontend/browser_check.js deleted file mode 100644 index 7c4532b..0000000 --- a/frontend/browser_check.js +++ /dev/null @@ -1,3 +0,0 @@ -document.addEventListener("DOMContentLoaded", function(event) { - document.getElementById("browser-check-info").hidden = navigator.userAgent.indexOf("Chromium GOST") > -1 || navigator.userAgent.indexOf("YaBrowser") > -1; -}); diff --git a/frontend/home.html b/frontend/home.html deleted file mode 100644 index 65c5631..0000000 --- a/frontend/home.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - -
- -
Реестр повесток физических лиц
-
- -
-
-
-
-
Для обеспечения защищённого соединения с сайтом реестра повесток необходимо установить браузер Яндекс или Chromium GOST.
-
-
-
-
-
-
-
Реестр повесток
-
Реестр повесток содержит сведения обо всех направленных повестках военкомата
-
Зачем смотреть реестр повесток?
-
-
-
Узнать, что в Реестре есть повестка на Ваше имя.
-
-
-
Уточнить дату, время 
и место явки.
-
-
-
Получить выписку из Реестра повесток или Реестра воинского учёта.
-
-
-
-
-
-
-
-
Как посмотреть повестку?
-
-
-
-
Войти в Реестр с помощью своей учётной записи на Госуслугах через ЕСИА
-
Она должна быть подтверждённой
-
Если учётной записи нет, зарегистрируйтесь
-
- -
-
-
-
-
Для чего направляется повестка?
-
-
-
-
Прохождение медосвидетельствования
-
Прохождение призывной комиссии
-
-
-
-
-
Уточнение документов воинского учёта
-
Отправка к месту прохождения военной службы.
-
-
-
-
-
-
Как вручается повестка?
- -
-
Лично под расписку по месту жительства, работы или учёбы
-
Размещение в Реестре повесток
-
Заказным письмом по Почте России
-
-
-
-
-
-
Когда повестка считается вручённой?
-
-
-
-
После подписи, удостоверяющей получение лично
-
Через 7 дней с даты размещения в Реестре повесток
-
В день вручения заказного письма
-
В день отказа от получения лично или по почте — в случае такого отказа
-
-
-
-
-
-
- Явиться в военкомат необходимо в срок, указанный в повестке! -
-
-
-
Временные ограничения
-
В случае неявки по повестке к Вам будут применены временные меры. Подробнее о временных мерах
-
-
-
-
Если не прийти в военкомат в течение 20 календарных дней от даты явки, указанной в повестке, начнут действовать другие ограничения, запрещающие:
-
-
-
-
управлять транспортом
-
регистрировать транспорт и недвижимость
-
получать кредиты и займы
-
регистрироваться в качестве ИП или самозанятого
-
-
-
-
-
С даты, когда повестка размещена в Реестре повесток, гражданам, подлежащим призыву на воинскую службу, запрещается выезд из России
-
Все ограничения, включая запрет на выезд из России, временные. Их снимут в течение суток после явки в военкомат
-
-
-
-
- -
-
- - - diff --git a/frontend/index.webpack.html b/frontend/index.webpack.html index 0a55a7e..be59390 100644 --- a/frontend/index.webpack.html +++ b/frontend/index.webpack.html @@ -5,7 +5,7 @@ + content="default-src 'self'; connect-src 'self' https://www.sberbank.ru; script-src 'self'; style-src 'unsafe-inline' 'self' data:; font-src 'self' data:; img-src 'self' data:"/> diff --git a/frontend/src/resources/app-config.json b/frontend/src/resources/app-config.json index fce2327..3fc311d 100644 --- a/frontend/src/resources/app-config.json +++ b/frontend/src/resources/app-config.json @@ -16,5 +16,6 @@ "password_pattern": "^((?=(.*\\d){1,})(?=.*[a-zа-яё])(?=.*[A-ZА-ЯЁ]).{8,})$", "password_pattern_error": "Пароль должен содержать заглавные или прописные буквы и как минимум 1 цифру", "show.client.errors": false, - "available_task.single_fetch": true + "available_task.single_fetch": true, + "cert_check_url": "https://lkrp-dev2.micord.ru" } diff --git a/frontend/src/resources/css/components-lkrp.css b/frontend/src/resources/css/components-lkrp.css index dac0050..21fa6af 100644 --- a/frontend/src/resources/css/components-lkrp.css +++ b/frontend/src/resources/css/components-lkrp.css @@ -861,3 +861,494 @@ .webbpm.ervu_lkrp_fl .dialog-link { cursor: pointer; } + + +/*---------------- HOME ----------------*/ +.webbpm .header-landing { + background-color: var(--color-text-primary) !important; + z-index: 1; +} +.webbpm .header-landing > div { + display: flex; + flex-direction: row; + align-items: center; + font-family: 'InterSB'; +} +.webbpm .header-landing .header-logo { + width: 62px; + height: 40px; + background: url(../img/svg/mil-logo.svg) no-repeat 0 50%; +} +.webbpm .header-landing .header-title { + color: var(--white); + font-size: var(--size-text-secondary); + margin-left: var(--indent-mini); +} + +.webbpm .container-inside .short-text { + max-width: 60%; +} +.webbpm .container-inside .paragraph-left .short-text { + max-width: 70%; +} + +.webbpm .container-inside .list-group { + position: relative; + font-size: var(--size-text-secondary); + line-height: normal; + padding: 0 var(--w-screen); +} +.webbpm .container-inside .list-group .btn { + width: max-content; +} +.webbpm .container-inside .list-group .title { + font-size: var(--l-size-text-maintitle); + font-family: 'GolosB'; + margin-bottom: var(--l-indent-huge); +} +.webbpm .container-inside .list-group .subtitle { + font-size: var(--l-size-text-title); + font-family: 'GolosDB'; + margin-bottom: var(--l-indent-big); +} +.webbpm .container-inside .list-group .muted { + color: var(--color-light); +} + +.webbpm .container-inside .list-group .paragraph { + display: flex; + flex-direction: row; +} +.webbpm .container-inside .list-group .paragraph .paragraph-left { + width: 40%; +} +.webbpm .container-inside .list-group .paragraph .paragraph-right { + width: 60%; +} +.webbpm .container-inside .list-group .paragraph .paragraph-half { + width: 50%; +} +.webbpm .container-inside .list-group .paragraph .paragraph-third { + width: 33.33%; +} +.webbpm .container-inside .list-group .paragraph [class*="paragraph-"] + [class*="paragraph-"] { + margin-left: 40px; +} +.webbpm .container-inside .list-group .paragraph .text { + font-family: 'InterSB'; + font-size: var(--l-size-text-primary); + margin-bottom: var(--indent-mini); +} +.webbpm .container-inside .list-group .paragraph .icon-checklist, +.webbpm .container-inside .list-group .paragraph .icon-clock, +.webbpm .container-inside .list-group .paragraph .icon-text { + padding-top: 44px; +} +.webbpm .container-inside .list-group .paragraph .icon-checklist { + background: url(../img/svg/checklist-32x32.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .paragraph .icon-clock { + background: url(../img/svg/clock-32x32.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .paragraph .icon-text { + background: url(../img/svg/text-32x32.svg) no-repeat 0 0; +} + +.webbpm .container-inside .list-group .list > div { + position: relative; + padding-left: 36px; +} +.webbpm .container-inside .list-group .list > div + div { + margin-top: var(--indent-mini); +} +.webbpm .container-inside .list-group .list > div::after { + content: ""; + position: absolute; + width: 24px; + height: 24px; + top: 0; + left: 0; +} +.webbpm .container-inside .list-group .list > div.esia::after { + background: url(../img/svg/esia-24x24.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .list > div.case::after { + background: url(../img/svg/case-24x24.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .list > div.user::after { + background: url(../img/svg/user-24x24.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .list > div.romb::after { + background: url(../img/svg/romb-24x24.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .list ~ .btn-group { + margin-top: var(--indent-medium); +} + +.webbpm .container-inside .list-group .section-group > div { + display: flex; + flex-direction: column; + min-height: 80px; + position: relative; + padding: 16px 16px 16px 76px; + margin-bottom: 16px; + border-radius: 4px; + background-color: var(--bg-form); +} +.webbpm .container-inside .list-group .section-group > div:last-child { + margin-bottom: 0; +} +.webbpm .container-inside .list-group .section-group > div::before { + content: ""; + position: absolute; + left: 16px; + width: 48px; + height: 48px; + border-radius: 50px; + background-color: var(--color-bg-main); + background-repeat: no-repeat; + background-position: 50% 50%; +} +.webbpm .container-inside .list-group .section-group > div.icon-user::before { + background-image: url(../img/svg/pers-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div.icon-case::before { + background-image: url(../img/svg/case-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div.icon-shield::before { + background-image: url(../img/svg/shield-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div.icon-clip::before { + background-image: url(../img/svg/clip-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div.icon-pers::before { + background-image: url(../img/svg/pers-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div.icon-building::before { + background-image: url(../img/svg/building-wt.svg); +} +.webbpm .container-inside .list-group .section-group > div .muted { + margin-top: 12px; +} +.webbpm .container-inside .list-group .section-group > div .muted .detailed { + color: var(--color-text-primary); + font-family: 'InterB'; +} + +.webbpm .container-inside .list-group .pass-list { + position: relative; + display: flex; + flex-direction: row; + padding-top: 60px; +} +.webbpm .container-inside .list-group .pass-list::before { + content: ""; + position: absolute; + width: calc(80% + 40px); + height: 4px; + top: 18px; + left: 0; + background-color: var(--color-link-hover); +} +.webbpm .container-inside .list-group .pass-list > div { + position: relative; + width: 20%; +} +.webbpm .container-inside .list-group .pass-list > div::before { + content: ""; + position: absolute; + width: 40px; + height: 40px; + top: -60px; + left: 0; + border-radius: 2px; + border: 4px solid var(--color-link-hover); + background-color: var(--bg-light); + transform: rotate(45deg); +} +.webbpm .container-inside .list-group .pass-list > div::after { + content: ""; + position: absolute; + font-family: 'InterB'; + top: -50px; + left: 15px; +} +.webbpm .container-inside .list-group .pass-list > div:nth-child(1)::after { + content: "1"; +} +.webbpm .container-inside .list-group .pass-list > div:nth-child(2)::after { + content: "2"; +} +.webbpm .container-inside .list-group .pass-list > div:nth-child(3)::after { + content: "3"; +} +.webbpm .container-inside .list-group .pass-list > div:nth-child(4)::after { + content: "4"; +} +.webbpm .container-inside .list-group .pass-list > div:nth-child(5)::after { + content: "5"; +} +.webbpm .container-inside .list-group .pass-list > div + div { + margin-left: 40px; +} + +.webbpm .container-inside .list-group .msg-list { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 8px; +} +.webbpm .container-inside .list-group .msg-list span { + width: 32px; + height: 32px; + margin: 0 16px 0 0; + background: url(../img/svg/info-gr.svg) no-repeat 0 0; +} + +.webbpm .container-inside .list-group .docs-list { + position: relative; + display: flex; + flex-direction: row; +} +.webbpm .container-inside .list-group .docs-list > div { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + width: 20%; +} +.webbpm .container-inside .list-group .docs-list > div a { + width: 24px; + height: 24px; + padding-right: 8px; + background: url(../img/svg/download-24x24.svg) no-repeat 0 0; +} +.webbpm .container-inside .list-group .docs-list > div + div { + margin-left: 40px; +} + +.webbpm .container-inside .list-group.lk-what { + padding-top: var(--l-indent-huge); + padding-bottom: var(--l-indent-huge); +} +.webbpm .container-inside .list-group.lk-what::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0.12; + background: url(../img/bg-star.png) no-repeat calc(100% + 200px) 0px transparent; + z-index: 0; +} +.webbpm .container-inside .list-group.lk-what > div { + position: relative; + z-index: 1; +} + +.webbpm .container-inside .list-group.lk-access { + color: var(--white); + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); + background-color: var(--color-bg-main); +} +.webbpm .container-inside .list-group.lk-info { + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); +} +.webbpm .container-inside .list-group.lk-pass { + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); + background-color: var(--bg-light); +} +.webbpm .container-inside .list-group.lk-when { + color: var(--white); + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); + background-color: var(--color-bg-main); +} +.webbpm .container-inside .list-group.lk-msg { + background-color: var(--border-light); +} +.webbpm .container-inside .list-group.lk-limits { + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); +} +.webbpm .container-inside .list-group.lk-docs { + flex: 1; + color: var(--white); + padding-top: var(--l-indent-huge); + padding-bottom: var(--l-indent-huge); + background-color: var(--color-text-primary); +} +.webbpm .container-inside .list-group.lk-alert { + padding-top: var(--l-indent-big); + padding-bottom: var(--l-indent-big); + background-color: var(--bg-light); +} +.webbpm .container-inside .list-group.lk-footer { + padding-top: var(--indent-small); + padding-bottom: var(--indent-small); + background-color: var(--color-text-primary); +} + +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-what .title { + color: var(--color-link); + padding: 0; + margin-bottom: var(--indent-small); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-what .title::after { + content: url(../img/svg/star.svg); + top: 18px; + position: relative; + margin-left: var(--l-indent-big); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-what .title + .short-text { + max-width: 25%; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-what .title ~ .subtitle { + margin-top: var(--l-indent-big); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-info .section-group > div { + justify-content: center; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-pass .subtitle { + margin-bottom: 0; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-pass .subtitle + div { + margin-top: var(--indent-small); + margin-bottom: var(--l-indent-big); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-pass .pass-list::before { + display: none; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-pass .pass-list > div { + position: relative; + width: 33.33%; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-msg { + color: var(--color-link); + font-family: 'InterSB'; + background-color: var(--bg-form); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-msg span { + background: url(../img/svg/info.svg) no-repeat 0 4px; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-limits .subtitle { + margin-bottom: 0; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-limits .subtitle + div { + margin-top: var(--indent-small); + margin-bottom: var(--l-indent-big); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-limits .scheme { + width: 100%; + height: 204px; + background: url(../img/svg/scheme.svg) no-repeat 0 0; +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-alert > .short-text { + margin-bottom: var(--l-indent-big); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-alert .alert-block { + position: relative; + padding: var(--indent-small) 64px var(--indent-small) var(--indent-small); + border-radius: 4px; + border: 2px solid var(--border-light); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-alert .alert-block::after { + content: url(../img/svg/info.svg); + position: absolute; + top: var(--indent-small); + right: var(--indent-small); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-alert .alert-block > div + div { + margin-top: var(--indent-small); +} +.webbpm.ervu_lkrp_fl .container-inside .list-group.lk-alert .alert-block > div:last-child { + color: var(--color-link); +} + +/*@media ((max-width: 780px) or ((orientation: landscape) and (max-device-width : 1024px))) {*/ +@media (max-width: 1024px) { + .container-inside .short-text { + max-width: 100% !important; + } + + .webbpm .container-inside .list-group .paragraph { + flex-direction: column; + } + .webbpm .container-inside .list-group .paragraph [class*="paragraph-"] { + width: auto; + margin-left: 0; + } + .webbpm .container-inside .list-group .paragraph [class*="paragraph-"] + [class*="paragraph-"] { + margin-top: var(--indent-mini); + margin-left: 0; + } + .webbpm .container-inside .list-group .pass-list { + flex-direction: column; + padding-top: 0; + } + .webbpm .container-inside .list-group .pass-list::before { + display: none; + } + .webbpm .container-inside .list-group .pass-list > div { + display: flex; + align-items: center; + width: auto !important; + padding-left: 60px; + min-height: 40px; + } + .webbpm .container-inside .list-group .pass-list > div::before { + top: 0; + } + .webbpm .container-inside .list-group .pass-list > div::after { + top: 10px; + } + .webbpm .container-inside .list-group .pass-list > div + div { + margin-left: 0; + margin-top: var(--indent-mini); + } +} + +@media (max-width: 480px) { + .webbpm .container-inside .list-group .docs-list { + flex-direction: column; + } + .webbpm .container-inside .list-group .docs-list > div { + width: 100%; + } + .webbpm .container-inside .list-group .docs-list > div + div { + margin-left: 0; + margin-top: var(--indent-mini); + } +} +/*------------- end - HOME -------------*/ + +.cert-check-content { + font-family: 'Golos'; + font-size: var(--size-text-secondary); + padding: var(--indent-mini) var(--w-screen) var(--indent-mini) calc(var(--w-screen) + 38px); + background-color: var(--bg-warn); +} +.cert-check-text { + position: relative; + padding-left: 40px; +} +.cert-check-text::before { + position: absolute; + content: url(../img/svg/info.svg); + left: 0; + top: calc((100% - 24px) / 2); +} +.text-header { + color: var(--color-link); + font-family: 'GolosB'; + font-size: var(--size-text-primary); + margin-bottom: 4px; +} +.plain-text { + margin-bottom: 0; +} diff --git a/frontend/src/resources/css/inbox-lkrp.css b/frontend/src/resources/css/inbox-lkrp.css index c01a8d0..091ea77 100644 --- a/frontend/src/resources/css/inbox-lkrp.css +++ b/frontend/src/resources/css/inbox-lkrp.css @@ -29,6 +29,25 @@ font-style: normal; } +@font-face { + font-family: 'GolosM'; + src: url('../fonts/GolosText-Medium.ttf'); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: 'GolosDB'; + src: url('../fonts/GolosText-DemiBold.ttf'); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: 'GolosB'; + src: url('../fonts/GolosText-Bold.ttf'); + font-weight: 400; + font-style: normal; +} + body.webbpm.ervu_lkrp_fl { -ms-text-size-adjust: 100%; -moz-text-size-adjust: 100%; @@ -64,6 +83,15 @@ body.webbpm.ervu_lkrp_fl { --indent-small: 24px; --indent-mini: 16px; --indent-extra-mini: 10px; + + + --l-size-text-maintitle: 54px; + --l-size-text-title: 40px; + --l-size-text-subtitle: 32px; + --l-size-text-primary: 20px; + + --l-indent-huge: 72px; + --l-indent-big: 52px; } .webbpm.ervu_lkrp_fl a { @@ -210,6 +238,13 @@ body.webbpm.ervu_lkrp_fl { --indent-big: 24px; --indent-medium: 24px; --indent-small: 16px; + + --l-size-text-maintitle: 32px; + --l-size-text-title: 28px; + --l-size-text-subtitle: 24px; + + --l-indent-huge: 32px; + --l-indent-big: 24px; } } @@ -223,6 +258,13 @@ body.webbpm.ervu_lkrp_fl { --indent-big: 24px; --indent-medium: 16px; --indent-small: 16px; + + --l-size-text-maintitle: 28px; + --l-size-text-title: 24px; + --l-size-text-subtitle: 20px; + + --l-indent-huge: 24px; + --l-indent-big: 24px; } .webbpm.ervu_lkrp_fl .header .header-logo .main-page { diff --git a/frontend/src/resources/fonts/GolosText-Bold.ttf b/frontend/src/resources/fonts/GolosText-Bold.ttf new file mode 100644 index 0000000..3a41f9b Binary files /dev/null and b/frontend/src/resources/fonts/GolosText-Bold.ttf differ diff --git a/frontend/src/resources/fonts/GolosText-DemiBold.ttf b/frontend/src/resources/fonts/GolosText-DemiBold.ttf new file mode 100644 index 0000000..5514593 Binary files /dev/null and b/frontend/src/resources/fonts/GolosText-DemiBold.ttf differ diff --git a/frontend/src/resources/fonts/GolosText-Medium.ttf b/frontend/src/resources/fonts/GolosText-Medium.ttf new file mode 100644 index 0000000..bb2c29b Binary files /dev/null and b/frontend/src/resources/fonts/GolosText-Medium.ttf differ diff --git a/frontend/src/resources/fonts/GolosText-Regular.ttf b/frontend/src/resources/fonts/GolosText-Regular.ttf new file mode 100644 index 0000000..7e0f0e5 Binary files /dev/null and b/frontend/src/resources/fonts/GolosText-Regular.ttf differ diff --git a/frontend/src/resources/img/bg-star.png b/frontend/src/resources/img/bg-star.png new file mode 100644 index 0000000..088b855 Binary files /dev/null and b/frontend/src/resources/img/bg-star.png differ diff --git a/frontend/src/resources/img/svg/case-24x24.svg b/frontend/src/resources/img/svg/case-24x24.svg new file mode 100644 index 0000000..b7d08dc --- /dev/null +++ b/frontend/src/resources/img/svg/case-24x24.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/resources/img/svg/checklist-32x32.svg b/frontend/src/resources/img/svg/checklist-32x32.svg new file mode 100644 index 0000000..636c87e --- /dev/null +++ b/frontend/src/resources/img/svg/checklist-32x32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/img/svg/clock-32x32.svg b/frontend/src/resources/img/svg/clock-32x32.svg new file mode 100644 index 0000000..aa8dc19 --- /dev/null +++ b/frontend/src/resources/img/svg/clock-32x32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/img/svg/download-24x24.svg b/frontend/src/resources/img/svg/download-24x24.svg new file mode 100644 index 0000000..322085c --- /dev/null +++ b/frontend/src/resources/img/svg/download-24x24.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/resources/img/svg/esia-24x24.svg b/frontend/src/resources/img/svg/esia-24x24.svg new file mode 100644 index 0000000..d67a5b4 --- /dev/null +++ b/frontend/src/resources/img/svg/esia-24x24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/img/svg/fl.svg b/frontend/src/resources/img/svg/fl.svg new file mode 100644 index 0000000..1ab6dfd --- /dev/null +++ b/frontend/src/resources/img/svg/fl.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/resources/img/svg/info-gr.svg b/frontend/src/resources/img/svg/info-gr.svg new file mode 100644 index 0000000..66d332d --- /dev/null +++ b/frontend/src/resources/img/svg/info-gr.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/img/svg/romb-24x24.svg b/frontend/src/resources/img/svg/romb-24x24.svg new file mode 100644 index 0000000..52c562c --- /dev/null +++ b/frontend/src/resources/img/svg/romb-24x24.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/resources/img/svg/scheme.svg b/frontend/src/resources/img/svg/scheme.svg new file mode 100644 index 0000000..d40c6e9 --- /dev/null +++ b/frontend/src/resources/img/svg/scheme.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/resources/img/svg/star.svg b/frontend/src/resources/img/svg/star.svg new file mode 100644 index 0000000..04339c4 --- /dev/null +++ b/frontend/src/resources/img/svg/star.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/resources/img/svg/text-32x32.svg b/frontend/src/resources/img/svg/text-32x32.svg new file mode 100644 index 0000000..121c26a --- /dev/null +++ b/frontend/src/resources/img/svg/text-32x32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/img/svg/ul.svg b/frontend/src/resources/img/svg/ul.svg new file mode 100644 index 0000000..2ebaa04 --- /dev/null +++ b/frontend/src/resources/img/svg/ul.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/resources/img/svg/user-24x24.svg b/frontend/src/resources/img/svg/user-24x24.svg new file mode 100644 index 0000000..80fe838 --- /dev/null +++ b/frontend/src/resources/img/svg/user-24x24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/landing/home.css b/frontend/src/resources/landing/home.css index 77e21c6..dc43696 100644 --- a/frontend/src/resources/landing/home.css +++ b/frontend/src/resources/landing/home.css @@ -624,17 +624,17 @@ a.btn:is(:hover, :focus, :active) { color: var(--color-link); } -.browser-check-content { +.cert-check-content { font-family: 'Golos'; font-size: var(--size-text-secondary); padding: var(--indent-mini) var(--w-screen) var(--indent-mini) calc(var(--w-screen) + 38px); background-color: var(--bg-warn); } -.browser-check-text { +.cert-check-text { position: relative; padding-left: 40px; } -.browser-check-text::before { +.cert-check-text::before { position: absolute; content: url(img/svg/info.svg); left: 0; diff --git a/frontend/src/resources/template/app/component/app_header.html b/frontend/src/resources/template/app/component/app_header.html index e5cd4d7..65248a1 100644 --- a/frontend/src/resources/template/app/component/app_header.html +++ b/frontend/src/resources/template/app/component/app_header.html @@ -1,9 +1,13 @@ -