diff --git a/backend/src/main/java/ervu/client/fileupload/WebDavClient.java b/backend/src/main/java/ervu/client/fileupload/WebDavClient.java index 6e7fee62..3a265ef3 100644 --- a/backend/src/main/java/ervu/client/fileupload/WebDavClient.java +++ b/backend/src/main/java/ervu/client/fileupload/WebDavClient.java @@ -11,6 +11,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Comparator; @@ -34,6 +35,7 @@ import ervu.model.webdav.Server; import org.apache.http.client.ClientProtocolException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -44,6 +46,8 @@ import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import ru.micord.ervu.security.esia.config.EsiaConfig; +import ru.micord.ervu.util.UrlUtils; /** * @author Alexandr Shalaginov @@ -60,6 +64,10 @@ public class WebDavClient { private String password; @Value("${webdav.bad_servers.cache.expire.seconds:120}") private long cacheExpireSec; + @Value("${request.timeout:20}") + private long requestTimeout; + @Value("${connection.timeout:10}") + private long connectionTimeout; private List servers; private LoadingCache badServersCache; @@ -167,22 +175,29 @@ public class WebDavClient { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password.toCharArray()); } - }).build(); + }) + .connectTimeout(Duration.ofSeconds(connectionTimeout)) + .build(); HttpRequest httpRequest = HttpRequest.newBuilder().uri(URI.create(url)) - .GET().build(); + .GET() + .timeout(Duration.ofSeconds(requestTimeout)) + .build(); HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream() ); - if (response.statusCode() == 200) { + long contentLength = response.headers() + .firstValueAsLong(HttpHeaders.CONTENT_LENGTH) + .orElse(0); InputStreamResource resource = new InputStreamResource(response.body()); - String encodedFilename = URLEncoder.encode(getFilenameFromUrl(url), StandardCharsets.UTF_8); + String encodedFilename = URLEncoder.encode(UrlUtils.extractFileNameFromUrl(url), StandardCharsets.UTF_8); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename ) + .contentLength(contentLength) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } @@ -196,10 +211,6 @@ public class WebDavClient { } } - private String getFilenameFromUrl(String url) { - String path = URI.create(url).getPath(); - return path.substring(path.lastIndexOf('/') + 1); - } @Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}")) public void deleteFilesOlderThan(long seconds, String url, String... extensions) throws IOException { diff --git a/backend/src/main/java/ervu/client/okopf/EsnsiOkopfClient.java b/backend/src/main/java/ervu/client/okopf/EsnsiOkopfClient.java index 619429e8..d0f46ff7 100644 --- a/backend/src/main/java/ervu/client/okopf/EsnsiOkopfClient.java +++ b/backend/src/main/java/ervu/client/okopf/EsnsiOkopfClient.java @@ -1,20 +1,24 @@ package ervu.client.okopf; -import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Objects; import java.util.stream.Collectors; import java.util.zip.ZipInputStream; +import ervu.exception.EsnsiException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; +import org.springframework.retry.support.RetrySynchronizationManager; import org.springframework.stereotype.Component; /** @@ -22,27 +26,43 @@ import org.springframework.stereotype.Component; */ @Component public class EsnsiOkopfClient { - private static final Logger logger = LoggerFactory.getLogger(EsnsiOkopfClient.class); + private static final Logger LOGGER = LoggerFactory.getLogger(EsnsiOkopfClient.class); @Value("${esnsi.okopf.url}") private String url; + @Value("${esnsi.okopf.connect.timeout:2}") + private long connectTimeout; + @Value("${esnsi.okopf.read.timeout:4}") + private long readTimeout; - @Retryable(value = IOException.class, maxAttemptsExpression = "${esnsi.okopf.retry.max.attempts.load:3}", backoff = - @Backoff(delayExpression = "${esnsi.okop.retry.delay.load:30000}")) - public String getJsonOkopFormData() { - try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); - ZipInputStream archiveStream = new ZipInputStream(in); - BufferedReader br = new BufferedReader( - new InputStreamReader(archiveStream, StandardCharsets.UTF_8))) { - if (Objects.nonNull(archiveStream.getNextEntry())) { - logger.info("Received an archive in response."); - return br.lines().collect(Collectors.joining(System.lineSeparator())); + @Retryable(value = {IOException.class, EsnsiException.class}, maxAttemptsExpression = "${esnsi.okopf.retry.max.attempts.load:3}", backoff = + @Backoff(delayExpression = "${esnsi.okop.retry.delay.load:3000}")) + public String getJsonOkopFormData() throws IOException { + int retryCount = RetrySynchronizationManager.getContext().getRetryCount() + 1; + LOGGER.info("Attempt #{} to load json okopf form data from URL: {}", retryCount, url); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout((int) Duration.ofSeconds(connectTimeout).toMillis()); + connection.setReadTimeout((int) Duration.ofSeconds(connectTimeout).toMillis()); + int statusCode = connection.getResponseCode(); + LOGGER.info("Connecting timeout set to {} ms, read timeout set to {}", connectTimeout, readTimeout); + if (statusCode >= 200 && statusCode <= 202) { + try (ZipInputStream archiveStream = new ZipInputStream(connection.getInputStream()); + BufferedReader br = new BufferedReader( + new InputStreamReader(archiveStream, StandardCharsets.UTF_8))) { + if (Objects.nonNull(archiveStream.getNextEntry())) { + LOGGER.info("Received an archive in response."); + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + LOGGER.info("Received an empty archive in response."); } - logger.info("Received an empty archive in response. Skipping load okpof file process"); - } - catch (SecurityException | IOException e) { - logger.error("Failed to send HTTP request {} or process the response for okopf file.", url, e); } + throw new EsnsiException("Http status: " + statusCode + " Skipping load okopf file process. URL: " + url); + } + + @Recover + public String recover(Exception e) { + LOGGER.error("All retry attempts for IO operation failed for URL: {}. Triggering fallback logic.", url, e); return null; } } + diff --git a/backend/src/main/java/ervu/exception/EsnsiException.java b/backend/src/main/java/ervu/exception/EsnsiException.java new file mode 100644 index 00000000..0ccb50de --- /dev/null +++ b/backend/src/main/java/ervu/exception/EsnsiException.java @@ -0,0 +1,18 @@ +package ervu.exception; + +/** + * @author Artyom Khakimullin + */ +public class EsnsiException extends RuntimeException { + public EsnsiException(String message, Throwable cause) { + super(message, cause); + } + + public EsnsiException(Throwable cause) { + super(cause); + } + + public EsnsiException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java b/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java index 25f28c08..883f749b 100644 --- a/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java +++ b/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java @@ -3,5 +3,5 @@ package ervu.model.fileupload; /** * @author r.latypov */ -public record DownloadResponse(OrgInfo orgInfo, FileInfo fileInfo) { +public record DownloadResponse(UploadOrgInfo orgInfo, FileInfo fileInfo) { } diff --git a/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java b/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java index 682a832f..a6f1e08f 100644 --- a/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java +++ b/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java @@ -6,15 +6,15 @@ import java.util.Objects; * @author Alexandr Shalaginov */ public class EmployeeInfoKafkaMessage { - private final OrgInfo orgInfo; + private final UploadOrgInfo orgInfo; private final FileInfo fileInfo; - public EmployeeInfoKafkaMessage(OrgInfo orgInfo, FileInfo fileInfo) { + public EmployeeInfoKafkaMessage(UploadOrgInfo orgInfo, FileInfo fileInfo) { this.orgInfo = orgInfo; this.fileInfo = fileInfo; } - public OrgInfo getOrgInfo() { + public UploadOrgInfo getOrgInfo() { return orgInfo; } @@ -38,7 +38,7 @@ public class EmployeeInfoKafkaMessage { @Override public String toString() { return "KafkaMessage{" + - "orgInfo=" + orgInfo + + "uploadOrgInfo=" + orgInfo + ", fileInfo=" + fileInfo + '}'; } diff --git a/backend/src/main/java/ervu/model/fileupload/FileInfo.java b/backend/src/main/java/ervu/model/fileupload/FileInfo.java index 5ee94d48..5141e433 100644 --- a/backend/src/main/java/ervu/model/fileupload/FileInfo.java +++ b/backend/src/main/java/ervu/model/fileupload/FileInfo.java @@ -11,6 +11,7 @@ public class FileInfo { private String fileName; private String filePatternCode; private String filePatternName; + private String fileSize; private String departureDateTime; private String timeZone; private FileStatus fileStatus; @@ -19,12 +20,14 @@ public class FileInfo { } public FileInfo(String fileId, String fileUrl, String fileName, String filePatternCode, - String filePatternName, String departureDateTime, String timeZone, FileStatus fileStatus) { + String filePatternName, String fileSize, String departureDateTime, String timeZone, + FileStatus fileStatus) { this.fileId = fileId; this.fileUrl = fileUrl; this.fileName = fileName; this.filePatternCode = filePatternCode; this.filePatternName = filePatternName; + this.fileSize = fileSize; this.departureDateTime = departureDateTime; this.timeZone = timeZone; this.fileStatus = fileStatus; @@ -50,6 +53,10 @@ public class FileInfo { return filePatternName; } + public String getFileSize() { + return fileSize; + } + public String getDepartureDateTime() { return departureDateTime; } @@ -73,13 +80,14 @@ public class FileInfo { fileName, fileInfo.fileName) && Objects.equals(filePatternCode, fileInfo.filePatternCode ) && Objects.equals(filePatternName, fileInfo.filePatternName) - && Objects.equals(departureDateTime, fileInfo.departureDateTime); + && Objects.equals(departureDateTime, fileInfo.departureDateTime) && + Objects.equals(fileSize, fileInfo.getFileSize()); } @Override public int hashCode() { return Objects.hash(fileId, fileUrl, fileName, filePatternCode, filePatternName, - departureDateTime + departureDateTime, fileSize ); } @@ -92,6 +100,7 @@ public class FileInfo { ", filePatternCode='" + filePatternCode + '\'' + ", filePatternName='" + filePatternName + '\'' + ", departureDateTime='" + departureDateTime + '\'' + + ", fileSize='" + fileSize + '\'' + '}'; } } diff --git a/backend/src/main/java/ervu/model/fileupload/FileStatus.java b/backend/src/main/java/ervu/model/fileupload/FileStatus.java index 0506631b..a34038a2 100644 --- a/backend/src/main/java/ervu/model/fileupload/FileStatus.java +++ b/backend/src/main/java/ervu/model/fileupload/FileStatus.java @@ -1,12 +1,10 @@ package ervu.model.fileupload; -import java.io.Serializable; /** * @author Eduard Tihomirov */ public class FileStatus { - private String code; private String status; private String description; diff --git a/backend/src/main/java/ervu/model/fileupload/OrgInfo.java b/backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java similarity index 55% rename from backend/src/main/java/ervu/model/fileupload/OrgInfo.java rename to backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java index 767f3e3c..c35a533a 100644 --- a/backend/src/main/java/ervu/model/fileupload/OrgInfo.java +++ b/backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java @@ -7,18 +7,24 @@ import ru.micord.ervu.journal.SenderInfo; /** * @author Alexandr Shalaginov */ -public class OrgInfo { +public class UploadOrgInfo { private String orgName; private String orgId; private SenderInfo senderInfo; + private String esiaOrgId; - public OrgInfo() { + public UploadOrgInfo() { } - public OrgInfo(String orgName, String orgId, SenderInfo senderInfo) { + public UploadOrgInfo(String orgName, String orgId, SenderInfo senderInfo, String esiaOrgId) { this.orgName = orgName; this.orgId = orgId; this.senderInfo = senderInfo; + this.esiaOrgId = esiaOrgId; + } + + public String getEsiaOrgId() { + return esiaOrgId; } public String getOrgName() { @@ -37,23 +43,25 @@ public class OrgInfo { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - OrgInfo orgInfo = (OrgInfo) o; - return Objects.equals(orgName, orgInfo.orgName) && Objects.equals(orgId, - orgInfo.orgId - ) && Objects.equals(senderInfo, orgInfo.senderInfo); + UploadOrgInfo uploadOrgInfo = (UploadOrgInfo) o; + return Objects.equals(orgName, uploadOrgInfo.orgName) && Objects.equals(orgId, + uploadOrgInfo.orgId + ) && Objects.equals(senderInfo, uploadOrgInfo.senderInfo) + && Objects.equals(esiaOrgId, uploadOrgInfo.esiaOrgId); } @Override public int hashCode() { - return Objects.hash(orgName, orgId, senderInfo); + return Objects.hash(orgName, orgId, senderInfo, esiaOrgId); } @Override public String toString() { - return "OrgInfo{" + + return "UploadOrgInfo{" + "orgName='" + orgName + '\'' + ", orgId='" + orgId + '\'' + ", senderInfo='" + senderInfo + '\'' + + ", esiaOrgId='" + esiaOrgId + '\'' + '}'; } } diff --git a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java index 65069a7e..c9eb916e 100644 --- a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java +++ b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Locale; import java.util.UUID; @@ -31,14 +30,16 @@ import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.exception.JsonParsingException; import ru.micord.ervu.security.esia.model.EmployeeModel; import ru.micord.ervu.security.esia.model.PersonModel; import ru.micord.ervu.security.esia.service.UlDataService; -import ru.micord.ervu.security.esia.token.EsiaTokensStore; +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 ru.micord.ervu.service.InteractionService; +import ru.micord.ervu.util.DateUtils; import static ervu.enums.FileStatusCode.FILE_CLEAN; import static ervu.enums.FileStatusCode.FILE_INFECTED; @@ -52,13 +53,12 @@ import static ru.micord.ervu.util.StringUtils.convertToFio; @Service public class EmployeeInfoFileUploadService { private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeInfoFileUploadService.class); - private static final String FORMAT = "dd.MM.yyyy HH:mm:ss"; - private final WebDavClient webDavClient; private final EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService; private final KafkaTemplate kafkaTemplate; private final InteractionService interactionService; private final UlDataService ulDataService; + private final AuditService auditService; @Value("${av.kafka.message.topic.name}") private String kafkaTopicName; @@ -68,12 +68,14 @@ public class EmployeeInfoFileUploadService { EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService, @Qualifier("avTemplate") KafkaTemplate kafkaTemplate, InteractionService interactionService, - UlDataService ulDataService) { + UlDataService ulDataService, + AuditService auditService) { this.webDavClient = webDavClient; this.employeeInfoKafkaMessageService = employeeInfoKafkaMessageService; this.kafkaTemplate = kafkaTemplate; this.interactionService = interactionService; this.ulDataService = ulDataService; + this.auditService = auditService; } public boolean saveEmployeeInformationFile(MultipartFile multipartFile, String formType, @@ -89,7 +91,7 @@ public class EmployeeInfoFileUploadService { formType); String esiaUserId = userIdsPair.getEsiaUserId(); String ervuId = userIdsPair.getErvuId(); - String accessToken = EsiaTokensStore.getAccessToken(esiaUserId); + String accessToken = EsiaAuthInfoStore.getAccessToken(esiaUserId); EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken); PersonModel personModel = employeeModel.getPerson(); @@ -105,29 +107,34 @@ public class EmployeeInfoFileUploadService { ervuId ); + long fileSize = multipartFile.getSize(); + String departureDateTime = DateUtils.convertToString(now); + EmployeeInfoKafkaMessage kafkaMessage = employeeInfoKafkaMessageService.getKafkaMessage( + fileId, + fileUploadUrl, + fileName, + employeeInfoFileFormType, + departureDateTime, + accessToken, + offset, + fileStatus, + ervuId, + esiaUserId, + personModel, + fileSize + ); + if (fileUploadUrl != null) { fileStatus.setCode(FILE_UPLOADED.getCode()); fileStatus.setDescription("Файл принят до проверки на вирусы"); - String departureDateTime = now.format(DateTimeFormatter.ofPattern(FORMAT)); - String jsonMessage = getJsonKafkaMessage( - employeeInfoKafkaMessageService.getKafkaMessage( - fileId, - fileUploadUrl, - fileName, - employeeInfoFileFormType, - departureDateTime, - accessToken, - offset, - fileStatus, - ervuId, - esiaUserId, - personModel - ) - ); + String jsonMessage = getJsonKafkaMessage(kafkaMessage); return sendMessage(jsonMessage); } else { LOGGER.error("Failed to upload file: {}", fileName); + fileStatus.setCode(FILE_NOT_CHECKED.getCode()); + fileStatus.setDescription("Невозможно проверить файл по причине недоступности или ошибки в работе антивируса"); + auditService.processUploadEvent(kafkaMessage.getOrgInfo(), kafkaMessage.getFileInfo()); return false; } } @@ -206,6 +213,7 @@ public class EmployeeInfoFileUploadService { interactionService.delete(fileInfo.getFileId(), downloadResponse.orgInfo().getOrgId()); } else if (statusCode.equals(FILE_NOT_CHECKED.getCode())) { + auditService.processUploadEvent(downloadResponse.orgInfo(), downloadResponse.fileInfo()); interactionService.updateStatus(fileInfo.getFileId(), fileInfo.getFileStatus().getStatus(), downloadResponse.orgInfo().getOrgId() ); diff --git a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java index 8afc0242..d8414f2b 100644 --- a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java +++ b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java @@ -4,7 +4,7 @@ import ervu.model.fileupload.EmployeeInfoFileFormType; import ervu.model.fileupload.EmployeeInfoKafkaMessage; import ervu.model.fileupload.FileInfo; import ervu.model.fileupload.FileStatus; -import ervu.model.fileupload.OrgInfo; +import ervu.model.fileupload.UploadOrgInfo; import org.springframework.stereotype.Service; import ru.micord.ervu.journal.SenderInfo; import ru.micord.ervu.security.esia.model.OrganizationModel; @@ -25,7 +25,8 @@ public class EmployeeInfoKafkaMessageService { public EmployeeInfoKafkaMessage getKafkaMessage(String fileId, String fileUrl, String fileName, EmployeeInfoFileFormType formType, String departureDateTime, String accessToken, - String offset, FileStatus fileStatus, String ervuId, String prnOid, PersonModel personModel) { + String offset, FileStatus fileStatus, String ervuId, String prnOid, + PersonModel personModel, long fileSize) { return new EmployeeInfoKafkaMessage( getOrgInfo(accessToken, ervuId, prnOid, personModel), getFileInfo( @@ -35,33 +36,36 @@ public class EmployeeInfoKafkaMessageService { formType, departureDateTime, offset, - fileStatus + fileStatus, + fileSize ) ); } private FileInfo getFileInfo(String fileId, String fileUrl, String fileName, EmployeeInfoFileFormType formType, String departureDateTime, String offset, - FileStatus fileStatus) { + FileStatus fileStatus, long fileSize) { return new FileInfo( fileId, fileUrl, fileName, formType.getFilePatternCode(), formType.getFilePatternName(), + String.valueOf(fileSize), departureDateTime, offset, fileStatus ); } - private OrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) { + private UploadOrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) { OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken); SenderInfo senderInfo = new SenderInfo(); senderInfo.setFirstName(personModel.getFirstName()); senderInfo.setLastName(personModel.getLastName()); senderInfo.setMiddleName(personModel.getMiddleName()); senderInfo.setPrnOid(prnOid); - return new OrgInfo(organizationModel.getFullName(), ervuId, senderInfo); + return new UploadOrgInfo(organizationModel.getFullName(), ervuId, senderInfo, + organizationModel.getOid()); } } diff --git a/backend/src/main/java/ervu/service/scheduler/EsnsiOkopfSchedulerServiceImpl.java b/backend/src/main/java/ervu/service/scheduler/EsnsiOkopfSchedulerServiceImpl.java index 3774e385..bc47432a 100644 --- a/backend/src/main/java/ervu/service/scheduler/EsnsiOkopfSchedulerServiceImpl.java +++ b/backend/src/main/java/ervu/service/scheduler/EsnsiOkopfSchedulerServiceImpl.java @@ -32,7 +32,7 @@ import static org.springframework.util.StringUtils.hasText; @Service @DependsOn({"liquibase"}) public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerService { - private static final Logger logger = LoggerFactory.getLogger( + private static final Logger LOGGER = LoggerFactory.getLogger( EsnsiOkopfSchedulerServiceImpl.class); @Autowired @@ -48,11 +48,11 @@ public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerServic @Transactional public void init() { if (!cronLoad.equals(CRON_DISABLED)) { - logger.info("Synchronization with esnsi okopf enabled"); + LOGGER.info("Synchronization with esnsi okopf enabled"); load(); } else { - logger.info("Synchronization with esnsi okopf disabled"); + LOGGER.info("Synchronization with esnsi okopf disabled"); } } @@ -60,22 +60,24 @@ public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerServic @SchedulerLock(name = "loadOkopf") @Transactional public void load() { - logger.info("Loading okopf file"); + LOGGER.info("Loading okopf file"); try { String data = esnsiOkopfClient.getJsonOkopFormData(); if (hasText(data)) { - logger.info("Parsing from json file to okopf model"); + LOGGER.info("Parsing from json file to okopf model"); OkopfOrgModel orgModel = mapper.readValue(data, OkopfOrgModel.class); int currentVersion = mapper.readTree(data).findValue("version").asInt(); List okopfRecords = mapToOkopfRecords(orgModel.getData(), currentVersion); - logger.info("Finished parsing from json file to okopf model"); - logger.info("Loaded {} okopf records with version {}. Beginning to save okopf data", okopfRecords.size(), currentVersion); + LOGGER.info("Finished parsing from json file to okopf model"); + LOGGER.info("Loaded {} okopf records with version {}. Beginning to save okopf data", okopfRecords.size(), currentVersion); okopfDao.saveOrUpdate(okopfRecords); - logger.info("Successfully saved okopf data"); + LOGGER.info("Successfully saved okopf data"); + } else { + LOGGER.info("Data is empty"); } } catch (Exception e) { - logger.error("Failed to load okopf data", e); + LOGGER.error("Failed to load okopf data", e); } } 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 00000000..67b8ea0e --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java @@ -0,0 +1,89 @@ +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}") + 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 00000000..1bc3792a --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java @@ -0,0 +1,41 @@ +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 = "UL"; + public static final String LOGOUT_EVENT_TYPE = "logout"; + public static final String LOGIN_EVENT_TYPE = "login"; + public static final String SUCCESS_STATUS_TYPE = "success"; + public static final String FAILURE_STATUS_TYPE = "failure"; + + private static final Map routeDescriptions = Map.of( + "/", "Личный кабинет ЮР лица", + "/mydata", "Информация об организации", + "/filesentlog", "Журнал взаимодействия" + ); + + private static final Map downloadTypes = Map.of( + 1, "Выписка из журнала взаимодействия ЮЛ" + ); + + + private AuditConstants() { + } + + public static String getRouteDescription(String route) { + return Optional.ofNullable(routeDescriptions.get(route)) + .orElseThrow(() -> new IllegalArgumentException("Invalid route :" + route)); + } + + public static String getDownloadType(int formatRegistry) { + return Optional.ofNullable(downloadTypes.get(formatRegistry)) + .orElseThrow( + () -> new IllegalArgumentException("Invalid formatRegistry :" + formatRegistry)); + } +} 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 00000000..3fe6745c --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java @@ -0,0 +1,33 @@ +package ru.micord.ervu.audit.controller; + + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +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; + } + + @RequestMapping(value = "/action", method = RequestMethod.POST) + 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 00000000..95b1a0d3 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java @@ -0,0 +1,67 @@ +package ru.micord.ervu.audit.model; + + +import java.util.List; + +/** + * @author Adel Kalimullin + */ +public class AuditActionEvent extends AuditEvent { + private String eventType; + private String description; + private String sourceUrl; + private List searchAttributes; + private String fileName; + + public AuditActionEvent( + String esiaOrgId, String esiaPersonId, String eventTime, + String eventType, String description, String sourceUrl, + List searchAttributes, String fileName) { + super(esiaOrgId, esiaPersonId, eventTime); + this.eventType = eventType; + this.description = description; + this.sourceUrl = sourceUrl; + this.searchAttributes = searchAttributes; + 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; + } + + public List getSearchAttributes() { + return searchAttributes; + } + + public void setSearchAttributes(List searchAttributes) { + this.searchAttributes = searchAttributes; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} 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 00000000..7a6bf10f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java @@ -0,0 +1,56 @@ +package ru.micord.ervu.audit.model; + +import java.util.Map; + + +/** + * @author Adel Kalimullin + */ +public class AuditActionRequest { + private String eventType; + private String route; + private String sourceUrl; + private Map filterInfo; + private String fileName; + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + 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 getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Map getFilterInfo() { + return filterInfo; + } + + public void setFilterInfo( + Map filterInfo) { + this.filterInfo = filterInfo; + } +} 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 00000000..656520c2 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java @@ -0,0 +1,128 @@ +package ru.micord.ervu.audit.model; + + +/** + * @author Adel Kalimullin + */ + +public class AuditAuthorizationEvent extends AuditEvent { + private String status; + private String eventType; + private String organizationName; + private String firstName; + private String lastName; + private String middleName; + private String inn; + private String serverIp; + private String serverHostName; + private String clientIp; + private String clientHostName; + + public AuditAuthorizationEvent( + String esiaOrgId, String esiaPersonId, String eventTime, + String organizationName, String firstName, String lastName, + String middleName, String inn, String status, + String eventType, String serverIp, String serverHostName, + String clientIp, String clientHostName) { + super(esiaOrgId, esiaPersonId, eventTime); + this.status = status; + this.eventType = eventType; + this.organizationName = organizationName; + this.firstName = firstName; + this.lastName = lastName; + this.middleName = middleName; + this.inn = inn; + this.serverIp = serverIp; + this.serverHostName = serverHostName; + this.clientIp = clientIp; + this.clientHostName = clientHostName; + } + + 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 getOrganizationName() { + return organizationName; + } + + public void setOrganizationName(String organizationName) { + this.organizationName = organizationName; + } + + 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 getInn() { + return inn; + } + + public void setInn(String inn) { + this.inn = inn; + } + + 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 00000000..36c22145 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java @@ -0,0 +1,66 @@ +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 esiaOrgId, String esiaPersonId, String eventTime, + String downloadType, String fileName, String s3FileUrl, + String fileSize, String status) { + super(esiaOrgId, esiaPersonId, eventTime); + this.downloadType = downloadType; + this.fileName = fileName; + this.s3FileUrl = s3FileUrl; + this.fileSize = fileSize; + this.status = status; + } + + 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 getS3FileUrl() { + return s3FileUrl; + } + + public void setS3FileUrl(String s3FileUrl) { + this.s3FileUrl = s3FileUrl; + } + + 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; + } +} 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 00000000..a47ea802 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java @@ -0,0 +1,48 @@ +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 esiaOrgId; + protected String esiaPersonId; + protected String eventTime; + + public AuditEvent( + String esiaOrgId, String esiaPersonId, String eventTime) { + this.esiaOrgId = esiaOrgId; + this.esiaPersonId = esiaPersonId; + this.eventTime = eventTime; + } + + public String getSubsystem() { + return subsystem; + } + + public String getEsiaOrgId() { + return esiaOrgId; + } + + public void setEsiaOrgId(String esiaOrgId) { + this.esiaOrgId = esiaOrgId; + } + + public String getEsiaPersonId() { + return esiaPersonId; + } + + public void setEsiaPersonId(String esiaPersonId) { + this.esiaPersonId = esiaPersonId; + } + + 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/model/AuditUploadEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java new file mode 100644 index 00000000..65270471 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java @@ -0,0 +1,35 @@ +package ru.micord.ervu.audit.model; + + +import ervu.model.fileupload.FileInfo; +import ervu.model.fileupload.UploadOrgInfo; + +/** + * @author Adel Kalimullin + */ + +public class AuditUploadEvent { + private UploadOrgInfo orgInfo; + private FileInfo fileInfo; + + public AuditUploadEvent(UploadOrgInfo orgInfo, FileInfo fileInfo) { + this.orgInfo = orgInfo; + this.fileInfo = fileInfo; + } + + public UploadOrgInfo getOrgInfo() { + return orgInfo; + } + + public void setOrgInfo(UploadOrgInfo orgInfo) { + this.orgInfo = orgInfo; + } + + public FileInfo getFileInfo() { + return fileInfo; + } + + public void setFileInfo(FileInfo fileInfo) { + this.fileInfo = fileInfo; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java b/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java new file mode 100644 index 00000000..631e0508 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java @@ -0,0 +1,25 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class FilterCondition { + private String filterValue; + private String filterType; + + public String getFilterValue() { + return filterValue; + } + + public void setFilterValue(String filterValue) { + this.filterValue = filterValue; + } + + public String getFilterType() { + return filterType; + } + + public void setFilterType(String filterType) { + this.filterType = filterType; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java b/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java new file mode 100644 index 00000000..ec3d9fa5 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java @@ -0,0 +1,28 @@ +package ru.micord.ervu.audit.model; + +import java.util.List; + +/** + * @author Adel Kalimullin + */ + +public class FilterInfo { + private String conditionOperator; + private List conditions; + + public String getConditionOperator() { + return conditionOperator; + } + + public void setConditionOperator(String conditionOperator) { + this.conditionOperator = conditionOperator; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java b/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java new file mode 100644 index 00000000..1342bc95 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java @@ -0,0 +1,30 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class SearchCriteria { + private String searchAttribute; + private String searchValue; + + public SearchCriteria(String searchAttribute, String searchValue) { + this.searchAttribute = searchAttribute; + this.searchValue = searchValue; + } + + public String getSearchAttribute() { + return searchAttribute; + } + + public void setSearchAttribute(String searchAttribute) { + this.searchAttribute = searchAttribute; + } + + public String getSearchValue() { + return searchValue; + } + + public void setSearchValue(String searchValue) { + this.searchValue = searchValue; + } +} 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 00000000..f8992b44 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java @@ -0,0 +1,9 @@ +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 00000000..d9f9ea8d --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java @@ -0,0 +1,23 @@ +package ru.micord.ervu.audit.service; + +import javax.servlet.http.HttpServletRequest; + +import ervu.model.fileupload.FileInfo; +import ervu.model.fileupload.UploadOrgInfo; +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.kafka.model.OrgInfo; + +/** + * @author Adel Kalimullin + */ +public interface AuditService { + void processActionEvent(HttpServletRequest request, AuditActionRequest auditActionRequest); + + void processAuthEvent(HttpServletRequest request, OrgInfo orgInfo, String prnOid, String status, + String eventType); + + void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo fileInfo); + + void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName, + int formatRegistry, String status, String s3FileUrl); +} 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 00000000..9138f48d --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java @@ -0,0 +1,43 @@ +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 00000000..bac1e9a5 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java @@ -0,0 +1,177 @@ +package ru.micord.ervu.audit.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ervu.model.fileupload.FileInfo; +import ervu.model.fileupload.UploadOrgInfo; +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.*; +import ru.micord.ervu.audit.service.AuditKafkaPublisher; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.exception.JsonParsingException; +import ru.micord.ervu.kafka.model.OrgInfo; +import ru.micord.ervu.security.esia.model.EsiaAccessToken; +import ru.micord.ervu.security.esia.service.UlDataService; +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 auditKafkaPublisher; + private final JwtTokenService jwtTokenService; + private final UlDataService ulDataService; + private final ObjectMapper objectMapper; + @Value("${audit.kafka.authorization.topic}") + private String authorizationTopic; + @Value("${audit.kafka.action.topic}") + private String actionTopic; + @Value("${audit.kafka.file.upload.topic}") + private String fileUploadTopic; + @Value("${audit.kafka.file.download.topic}") + private String fileDownloadTopic; + + public BaseAuditService(AuditKafkaPublisher auditKafkaPublisher, JwtTokenService jwtTokenService, + UlDataService ulDataService, ObjectMapper objectMapper) { + this.auditKafkaPublisher = auditKafkaPublisher; + this.jwtTokenService = jwtTokenService; + this.ulDataService = ulDataService; + this.objectMapper = objectMapper; + } + + @Override + public void processActionEvent(HttpServletRequest request, + AuditActionRequest auditActionRequest) { + String orgId = getEsiaOrgId(request); + String userAccountId = jwtTokenService.getUserAccountId(request); + String description = AuditConstants.getRouteDescription(auditActionRequest.getRoute()); + + List searchAttributes = null; + if (auditActionRequest.getFilterInfo() != null && !auditActionRequest.getFilterInfo().isEmpty()) { + searchAttributes = getSearchCriteriaList(auditActionRequest.getFilterInfo()); + } + + AuditActionEvent event = new AuditActionEvent( + orgId, + userAccountId, + DateUtils.getClientDateTimeWithZoneFromRequest(request), + auditActionRequest.getEventType(), + description, + auditActionRequest.getSourceUrl(), + searchAttributes, + auditActionRequest.getFileName() + ); + + String message = convertToMessage(event); + auditKafkaPublisher.publishEvent(actionTopic, message); + } + + @Override + public void processAuthEvent(HttpServletRequest request, OrgInfo orgInfo, String prnOid, + 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( + orgInfo.getOrgOid(), + prnOid, + DateUtils.getClientDateTimeWithZoneFromRequest(request), + orgInfo.getOrgFullName(), + orgInfo.getSenderInfo().getFirstName(), + orgInfo.getSenderInfo().getLastName(), + orgInfo.getSenderInfo().getMiddleName(), + orgInfo.getInn(), + status, + eventType, + serverIp, + serverHostName, + clientIp, + clientHostName + ); + + String message = convertToMessage(event); + auditKafkaPublisher.publishEvent(authorizationTopic, message); + } + + @Override + public void processUploadEvent(UploadOrgInfo orgInfo, FileInfo fileInfo) { + AuditUploadEvent auditUploadEvent = new AuditUploadEvent( + orgInfo, + fileInfo + ); + + String message = convertToMessage(auditUploadEvent); + auditKafkaPublisher.publishEvent(fileUploadTopic, message); + } + + @Override + public void processDownloadEvent( + HttpServletRequest request, long fileSize, String fileName, int formatRegistry, + String status, String s3FileUrl) { + String userAccountId = jwtTokenService.getUserAccountId(request); + + AuditDownloadEvent event = new AuditDownloadEvent( + getEsiaOrgId(request), + userAccountId, + DateUtils.getClientDateTimeWithZoneFromRequest(request), + AuditConstants.getDownloadType(formatRegistry), + fileName, + s3FileUrl, + String.valueOf(fileSize), + status + ); + + String message = convertToMessage(event); + auditKafkaPublisher.publishEvent(fileDownloadTopic, message); + } + + private String getEsiaOrgId(HttpServletRequest request) { + String accessToken = jwtTokenService.getAccessToken(request); + EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken); + String scope = esiaAccessToken.getScope(); + return scope.substring(scope.indexOf('=') + 1, scope.indexOf(' ')); + } + + public List getSearchCriteriaList(Map filterInfoMap) { + List searchCriteriaList = new ArrayList<>(); + + for (Map.Entry entry : filterInfoMap.entrySet()) { + String searchAttribute = entry.getKey(); + FilterInfo filterInfo = entry.getValue(); + String searchValue = filterInfo.getConditions().stream() + .map(condition -> condition.getFilterValue() + " " + condition.getFilterType()) + .collect(Collectors.joining(", ")); + + if (filterInfo.getConditionOperator() != null) { + searchValue += "| Operator: " + filterInfo.getConditionOperator(); + } + + SearchCriteria searchCriteria = new SearchCriteria(searchAttribute, searchValue); + searchCriteriaList.add(searchCriteria); + } + + return searchCriteriaList; + } + + private String convertToMessage(Object event) { + try { + return objectMapper.writeValueAsString(event); + } + catch (JsonProcessingException e) { + throw new JsonParsingException(e); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java b/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java index 22b80272..23bf4d2d 100644 --- a/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java +++ b/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class SenderInfo { - @JsonProperty("prnOid") private String prnOid; // идентификатор сотрудника в ЕСИА @JsonProperty("lastName") diff --git a/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java b/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java index 149f720f..e6a2b153 100644 --- a/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java +++ b/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java @@ -6,7 +6,7 @@ import java.time.LocalDateTime; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import ru.micord.ervu.util.DateUtil; +import ru.micord.ervu.util.DateUtils; public class DepartureDateTimeDeserializer extends JsonDeserializer { @@ -14,6 +14,6 @@ public class DepartureDateTimeDeserializer extends JsonDeserializer configProps = new HashMap<>(); configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 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); configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); configProps.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); diff --git a/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java b/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java index 10023186..e3e41e6a 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java @@ -3,23 +3,26 @@ package ru.micord.ervu.kafka.controller; import java.time.ZonedDateTime; import java.util.TimeZone; -import com.fasterxml.jackson.core.JsonProcessingException; +import javax.servlet.http.HttpServletRequest; + import com.fasterxml.jackson.databind.ObjectMapper; import ervu.client.fileupload.WebDavClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ru.micord.ervu.exception.JsonParsingException; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.kafka.exception.ExcerptException; +import ru.micord.ervu.kafka.exception.ExcerptResponseException; import ru.micord.ervu.kafka.model.Data; import ru.micord.ervu.kafka.model.ExcerptResponse; import ru.micord.ervu.kafka.service.ReplyingKafkaService; import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import ru.micord.ervu.util.UrlUtils; /** * @author Eduard Tihomirov @@ -30,6 +33,9 @@ public class ErvuKafkaController { @Autowired private ReplyingKafkaService replyingKafkaService; + @Autowired + private AuditService auditService; + @Autowired private WebDavClient webDavClient; @@ -43,10 +49,12 @@ public class ErvuKafkaController { private ObjectMapper objectMapper; @RequestMapping(value = "/kafka/excerpt") - public ResponseEntity getExcerptFile( - @RequestHeader("Client-Time-Zone") String clientTimeZone) { - + public ResponseEntity getExcerptFile(HttpServletRequest request) { + String fileUrl = null; + String fileName = null; + long fileSize = 0; try { + String clientTimeZone = request.getHeader("Client-Time-Zone"); UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); Data data = new Data(); data.setErvuId(userIdsPair.getErvuId()); @@ -59,16 +67,30 @@ public class ErvuKafkaController { ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); if (!excerptResponse.getSuccess()) { - throw new ExcerptException("Error with getting excerpt url " + excerptResponse.getMessage()); + throw new ExcerptResponseException( + "Error with getting excerpt url " + excerptResponse.getMessage()); } else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null || excerptResponse.getData().getFileUrl().isEmpty()) { + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); return ResponseEntity.noContent().build(); } - return webDavClient.webDavDownloadFile(excerptResponse.getData().getFileUrl()); + fileUrl = excerptResponse.getData().getFileUrl(); + fileName = UrlUtils.extractFileNameFromUrl(excerptResponse.getData().getFileUrl()); + ResponseEntity responseEntity = webDavClient.webDavDownloadFile(fileUrl); + fileSize = responseEntity.getHeaders().getContentLength(); + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.SUCCESS_STATUS_TYPE, fileUrl + ); + return responseEntity; } - catch (JsonProcessingException e) { - throw new JsonParsingException(e); + catch (Exception e) { + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + throw new ExcerptException(e); } } } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java index da4b4a7d..fbcfb685 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java @@ -4,10 +4,6 @@ package ru.micord.ervu.kafka.exception; * @author Adel Kalimullin */ public class ExcerptException extends RuntimeException { - public ExcerptException(String message) { - super(message); - } - public ExcerptException(String message, Throwable cause) { super(message, cause); } @@ -16,4 +12,7 @@ public class ExcerptException extends RuntimeException { super(cause); } + public ExcerptException(String message) { + super(message); + } } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java new file mode 100644 index 00000000..ee8cd409 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java @@ -0,0 +1,10 @@ +package ru.micord.ervu.kafka.exception; + +/** + * @author Adel Kalimullin + */ +public class ExcerptResponseException extends RuntimeException { + public ExcerptResponseException(String message) { + super(message); + } +} 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 00000000..7c99bd1c --- /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/impl/BaseReplyingKafkaServiceImpl.java b/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaServiceImpl.java index 7e103406..03e19f72 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaServiceImpl.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/service/impl/BaseReplyingKafkaServiceImpl.java @@ -1,16 +1,20 @@ 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.apache.kafka.common.header.internals.RecordHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.kafka.requestreply.ReplyingKafkaTemplate; import org.springframework.kafka.requestreply.RequestReplyFuture; import org.springframework.kafka.support.KafkaHeaders; import org.springframework.stereotype.Service; import ru.micord.ervu.kafka.exception.KafkaMessageException; +import ru.micord.ervu.kafka.exception.KafkaMessageReplyTimeoutException; import ru.micord.ervu.kafka.service.ReplyingKafkaService; /** @@ -18,7 +22,7 @@ import ru.micord.ervu.kafka.service.ReplyingKafkaService; */ @Service public class BaseReplyingKafkaServiceImpl implements ReplyingKafkaService { - + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final ReplyingKafkaTemplate replyingKafkaTemplate; public BaseReplyingKafkaServiceImpl( @@ -29,17 +33,23 @@ public class BaseReplyingKafkaServiceImpl implements ReplyingKafkaService { public String sendMessageAndGetReply(String requestTopic, String replyTopic, String requestMessage) { + long startTime = System.currentTimeMillis(); ProducerRecord record = new ProducerRecord<>(requestTopic, requestMessage); record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, replyTopic.getBytes())); RequestReplyFuture replyFuture = replyingKafkaTemplate.sendAndReceive(record); try { - return Optional.ofNullable(replyFuture.get()) + String result = Optional.ofNullable(replyFuture.get()) .map(ConsumerRecord::value) .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); } } } diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java index cc1bfd54..43236f99 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java @@ -31,7 +31,7 @@ import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT; @EnableWebSecurity public class SecurityConfig { private static final String[] PERMIT_ALL = new String[] { - "/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout", + "/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout" }; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; 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 e16d48ac..34655858 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,15 +6,17 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.CredentialsExpiredException; +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) { @@ -77,4 +79,25 @@ public class EsiaTokensStore { 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 937368d7..43bc96aa 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 @@ -38,10 +38,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}") @@ -62,6 +62,12 @@ 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 getEsiaOrgScopes() { String[] scopeItems = esiaOrgScopes.split(","); return String.join(" ", Arrays.stream(scopeItems).map(item -> orgScopeUrl + item.trim()).toArray(String[]::new)); @@ -125,4 +131,12 @@ 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 3a8ee09d..42056fa4 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 @@ -4,9 +4,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import ru.micord.ervu.security.SecurityConstants; import ru.micord.ervu.security.esia.model.OrgInfoModel; import ru.micord.ervu.security.esia.service.EsiaAuthService; import ru.micord.ervu.security.esia.service.EsiaDataService; @@ -27,15 +25,15 @@ public class EsiaController { private EsiaDataService esiaDataService; @GetMapping(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); } @PostMapping(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 00000000..7584b668 --- /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 f6a476e4..a20b88c2 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 00000000..299876f7 --- /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 70b6aa87..9bd82dcb 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 @@ -1,5 +1,6 @@ package ru.micord.ervu.security.esia.service; +import java.io.IOException; import java.lang.invoke.MethodHandles; import java.net.URI; import java.net.URL; @@ -9,25 +10,39 @@ 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.Arrays; 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 ervu.service.okopf.OkopfService; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; +import org.springframework.web.util.WebUtils; import ru.micord.ervu.security.esia.exception.EsiaException; -import ru.micord.ervu.security.esia.model.*; -import ru.micord.ervu.security.esia.token.EsiaTokensStore; +import ru.micord.ervu.security.esia.model.EmployeeModel; +import ru.micord.ervu.security.esia.model.EsiaAccessToken; +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.OrganizationModel; +import ru.micord.ervu.security.esia.model.SignResponse; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContext; import org.springframework.util.StringUtils; import ru.micord.ervu.security.esia.config.EsiaConfig; @@ -41,13 +56,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; 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.LocalizedException; +import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils; /** * @author Eduard Tihomirov @@ -55,6 +70,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; @Autowired @@ -69,18 +87,22 @@ public class EsiaAuthService { private OkopfService okopfService; @Autowired private SecurityHelper securityHelper; + @Autowired + private AuditService auditService; @Value("${ervu.kafka.org.reply.topic}") private String requestReplyTopic; @Value("${ervu.kafka.org.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"); @@ -92,13 +114,15 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); 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"; String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaCodeUrl(); @@ -149,23 +173,22 @@ 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; boolean hasRole = false; + long timeSignSecret = 0, timeRequestAccessToken = 0, timeVerifySecret = 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(); String scopeOrg = esiaConfig.getEsiaOrgScopes(); @@ -175,20 +198,20 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); 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); + timeSignSecret = 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("scope_org", scopeOrg) @@ -196,6 +219,7 @@ public class EsiaAuthService { .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)) @@ -205,6 +229,7 @@ public class EsiaAuthService { .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout())) .build() .send(postReq, HttpResponse.BodyHandlers.ofString()); + timeRequestAccessToken = System.currentTimeMillis() - startTime; String responseString = postResp.body(); EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class @@ -215,43 +240,54 @@ 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(); String esiaRefreshTokenStr = tokenResponse.getRefreshToken(); + startTime = System.currentTimeMillis(); String verifyResult = verifyToken(esiaAccessTokenStr); + timeVerifySecret = System.currentTimeMillis() - startTime; if (verifyResult != null) { throw new EsiaException(verifyResult); } EsiaAccessToken esiaAccessToken = ulDataService.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(), timeSignSecret, timeRequestAccessToken, timeVerifySecret); + } + OrgInfo orgInfo = null; + String status = null, ervuId = null; try { + orgInfo = getOrgInfo(esiaAccessTokenStr); hasRole = ulDataService.checkRole(esiaAccessTokenStr); - String ervuId = getErvuId(esiaAccessTokenStr, prnOid); - createTokenAndAddCookie(response, prnOid, ervuId, hasRole, expiresIn); + ervuId = getErvuId(prnOid, orgInfo); if (!hasRole) { LOGGER.error("The user with id = " + prnOid + " does not have the required role"); - return new ResponseEntity<>( - "Доступ запрещен. Пользователь должен быть включен в группу \"Сотрудник, ответственный за военно-учетную работу\" в ЕСИА", - HttpStatus.FORBIDDEN - ); + throw new LocalizedException("access_denied", MESSAGE_SOURCE); } - return ResponseEntity.ok("Authentication successful"); + status = AuditConstants.SUCCESS_STATUS_TYPE; } catch (Exception e) { - createTokenAndAddCookie(response, prnOid, null, hasRole , 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_TYPE; + if (e instanceof EsiaException || e instanceof JsonProcessingException) { + throw new EsiaException(e); + } + } + finally { + if (orgInfo!= null){ + auditService.processAuthEvent(request, orgInfo, prnOid, status, + AuditConstants.LOGIN_EVENT_TYPE); + } + createTokenAndAddCookie(response, prnOid, ervuId, hasRole , expiresIn); } } @@ -262,6 +298,7 @@ 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(); String scope = esiaConfig.getEsiaScopes(); String scopeOrg = esiaConfig.getEsiaOrgScopes(); @@ -271,13 +308,11 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); 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) @@ -318,17 +353,18 @@ public class EsiaAuthService { EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr); String prnOid = esiaAccessToken.getSbjId(); Long expiresIn = tokenResponse.getExpiresIn(); - EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); - EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn); - String ervuId = getErvuId(esiaAccessTokenStr, prnOid); + EsiaAuthInfoStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); + EsiaAuthInfoStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn); + OrgInfo orgInfo = getOrgInfo(esiaAccessTokenStr); + String ervuId = getErvuId(prnOid, orgInfo); createTokenAndAddCookie(response, esiaAccessToken.getSbjId(), ervuId, true, expiresIn); } - catch (Exception e) { + catch (EsiaException | IOException | InterruptedException e) { throw new EsiaException(e); } } - private SignResponse signMap(Map paramsToSign) { + private String signMap(Map paramsToSign) { try { StringBuilder toSign = new StringBuilder(); for (String s : paramsToSign.values()) { @@ -340,13 +376,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) { @@ -361,44 +398,67 @@ public class EsiaAuthService { } public String logout(HttpServletRequest request, HttpServletResponse response) { + OrgInfo orgInfo = null; + String userId = null; try { + userId = jwtTokenService.getUserAccountId(request); + String accessToken = EsiaAuthInfoStore.getAccessToken(userId); + orgInfo = getOrgInfo(accessToken); securityHelper.clearAccessCookies(response); - String userId = jwtTokenService.getUserAccountId(request); - EsiaTokensStore.removeAccessToken(userId); - EsiaTokensStore.removeRefreshToken(userId); + 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); + auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.SUCCESS_STATUS_TYPE, + AuditConstants.LOGOUT_EVENT_TYPE + ); return buildUrl(url, params); } catch (Exception e) { + if (orgInfo != null) { + auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.FAILURE_STATUS_TYPE, + AuditConstants.LOGOUT_EVENT_TYPE + ); + } throw new EsiaException(e); } } - public String getErvuId(String accessToken, String prnOid) { + public String getErvuId(String prnOid, OrgInfo orgInfo) throws JsonProcessingException { + orgInfo.setOrgTypeName(okopfService.findTitleByLeg(orgInfo.getOrgTypeLeg())); + String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, + requestReplyTopic, objectMapper.writeValueAsString(orgInfo) + ); + ErvuOrgResponse ervuOrgResponse = objectMapper.readValue(kafkaResponse, ErvuOrgResponse.class); + String ervuId = ervuOrgResponse.getData().getErvuId(); + if (!StringUtils.hasText(ervuId)) { + throw new EsiaException("No ervuId for prnOid = " + prnOid); + } + return ervuId; + } + + private OrgInfo getOrgInfo(String accessToken) { + long startTime = System.currentTimeMillis(); + long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0; try { OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken); + timeRequestPersonDataOrg = System.currentTimeMillis() - startTime; + startTime = System.currentTimeMillis(); EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken); + timeRequestPersonDataEmployee = System.currentTimeMillis() - startTime; + startTime = System.currentTimeMillis(); EmployeeModel chiefModel = ulDataService.getChiefEmployeeModel(accessToken); - OrgInfo orgInfo = copyToOrgInfo(organizationModel, employeeModel, chiefModel); - orgInfo.setOrgTypeName(okopfService.findTitleByLeg(orgInfo.getOrgTypeLeg())); - String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, - requestReplyTopic, objectMapper.writeValueAsString(orgInfo) - ); - ErvuOrgResponse ervuOrgResponse = objectMapper.readValue(kafkaResponse, ErvuOrgResponse.class); - String ervuId = ervuOrgResponse.getData().getErvuId(); - - if (!StringUtils.hasText(ervuId)) { - throw new EsiaException("No ervuId for prnOid = " + prnOid); - } - return ervuId; + timeRequestPersonDataChief = System.currentTimeMillis() - startTime; + return copyToOrgInfo(organizationModel, employeeModel, chiefModel); } - catch (Exception e) { - throw new EsiaException(e); + finally { + LOGGER.info("Thread {} - RequestPersonDataOrg: {} ms RequestPersonDataEmployee: {} ms RequestPersonDataChief: {} ms", + Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief + ); } } @@ -458,13 +518,6 @@ public class EsiaAuthService { return employee; } - 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, Boolean hasRole, Long expiresIn) { Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId, hasRole); @@ -484,6 +537,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"; } @@ -493,17 +549,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) { @@ -520,6 +575,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())) @@ -530,4 +586,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/token/TokensClearShedulerService.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java deleted file mode 100644 index 89c9db4c..00000000 --- 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/helper/SecurityHelper.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java index 9c966f60..6ce0411e 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,7 @@ public final class SecurityHelper { addResponseCookie(response, emptyAuthMarker); } - private void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { + public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } @@ -86,4 +86,10 @@ public final class SecurityHelper { .secure(accessCookieSecure) .sameSite(accessCookieSameSite); } + + public void clearCookie(HttpServletResponse response, String name, String path) { + ResponseCookie emptyCookie = createCookie(name, null, path) + .maxAge(0).build(); + addResponseCookie(response, emptyCookie); + } } 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 db9a7e81..3e388a90 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,8 @@ 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.exception.UnauthorizedException; +import ru.micord.ervu.security.esia.EsiaAuthInfoStore; import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.cg.webbpm.modules.resources.api.ResourceMetadataUtils; @@ -68,7 +68,7 @@ public class JwtTokenService { return false; } String[] ids = token.getUserAccountId().split(":"); - return EsiaTokensStore.validateAccessToken(ids[0]); + return EsiaAuthInfoStore.validateAccessToken(ids[0]); } public Token getToken(String token) { @@ -81,11 +81,11 @@ 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) { 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 52% 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 80644b92..c954a1dc 100644 --- a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java +++ b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java @@ -1,9 +1,10 @@ package ru.micord.ervu.util; -import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; +import javax.servlet.http.HttpServletRequest; + import org.springframework.util.StringUtils; import static org.springframework.util.StringUtils.hasText; @@ -11,12 +12,31 @@ import static org.springframework.util.StringUtils.hasText; /** * @author gulnaz */ -public final class DateUtil { - +public final class DateUtils { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_WITH_TIMEZONE_FORMATTER = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ss.SSSX"); - private DateUtil() {} + private DateUtils() { + } + + public static String getCurrentFormattedDateTimeWithZone(){ + ZonedDateTime now = ZonedDateTime.now(); + return now.format(DATE_TIME_WITH_TIMEZONE_FORMATTER); + } + + public static String getClientDateTimeWithZoneFromRequest(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) @@ -30,6 +50,10 @@ public final class DateUtil { : null; } + public static String convertToString(LocalDateTime dateTime) { + return dateTime == null ? "" : dateTime.format(DATE_TIME_FORMATTER); + } + public static String convertToString(LocalDate date) { return date == null ? "" : date.format(DATE_FORMATTER); } 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 00000000..5ee922a5 --- /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; + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/util/StringUtils.java b/backend/src/main/java/ru/micord/ervu/util/StringUtils.java index 2d3cb4a5..0fdfaff0 100644 --- a/backend/src/main/java/ru/micord/ervu/util/StringUtils.java +++ b/backend/src/main/java/ru/micord/ervu/util/StringUtils.java @@ -1,9 +1,13 @@ package ru.micord.ervu.util; + import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.substring; -public class StringUtils { +public final class StringUtils { + + private StringUtils() { + } public static String convertToFio(String firstName, String middleName, String lastName) { String firstNameInitial = substring(firstName, 0, 1).toUpperCase(); diff --git a/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java b/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java new file mode 100644 index 00000000..93982d3f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java @@ -0,0 +1,18 @@ +package ru.micord.ervu.util; + + +import java.net.URI; + +/** + * @author Adel Kalimullin + */ +public final class UrlUtils { + + private UrlUtils(){ + } + + public static String extractFileNameFromUrl(String url) { + String path = URI.create(url).getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } +} 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 00000000..d7091e89 --- /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 00000000..878998f5 --- /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 00000000..67508e9a --- /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 00000000..67508e9a --- /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 699d1a19..56e772c3 100644 --- a/config.md +++ b/config.md @@ -827,3 +827,17 @@ JBPM использует 3 корневых категории логирова - `ERVU_KAFKA_EXCERPT_REQUEST_TOPIC` - топик для записи запроса для получения выписки по журналу взаимодействия - `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки - `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения + + +#### Взаимодействие с Kafka audit +- `AUDIT_KAFKA_AUTHORIZATION_TOPIC` - топик для отправки аудита в журнал авторизации +- `AUDIT_KAFKA_ACTION_TOPIC` - топик для отправки аудита в журнал действий пользователя +- `AUDIT_KAFKA_FILE_UPLOAD_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 f73f53f9..72b96e0c 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 8011098a..31baf910 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 73ca176e..80a01839 100644 --- a/config/local.env +++ b/config/local.env @@ -31,7 +31,7 @@ ESIA_REDIRECT_URL=http://localhost:8080/ ESIA_UPLOAD_DATA_ROLE=MNSV89_UPLOAD_DATA #ESIA_CLIENT_CERT_HASH=04508B4B0B58776A954A0E15F574B4E58799D74C61EE020B3330716C203E3BDD ESIA_CLIENT_CERT_HASH=CF35A98C48E48665EA73530537BAFBB51F911C434ADC89215C2F86DCD04E28C5 -ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * +ESIA_AUTH_INFO_CLEAR_CRON=0 0 */1 * * * SIGN_URL=https://ervu-sign-dev.k8s.micord.ru/sign SIGN_VERIFY_URL=https://ervu-sign-dev.k8s.micord.ru/verify diff --git a/config/micord.env b/config/micord.env index cf6b43ce..070d9349 100644 --- a/config/micord.env +++ b/config/micord.env @@ -27,7 +27,7 @@ ESIA_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/ ESIA_LOGOUT_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/home.html ESIA_UPLOAD_DATA_ROLE=MNSV89_UPLOAD_DATA ESIA_CLIENT_CERT_HASH=04508B4B0B58776A954A0E15F574B4E58799D74C61EE020B3330716C203E3BDD -ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * +ESIA_AUTH_INFO_CLEAR_CRON=0 0 */1 * * * SIGN_URL=https://ervu-sign-dev.k8s.micord.ru/sign SIGN_VERIFY_URL=https://ervu-sign-dev.k8s.micord.ru/verify @@ -43,7 +43,21 @@ ERVU_KAFKA_EXCERPT_REQUEST_TOPIC=ervu.lkrp.excerpt.request ESNSI_OKOPF_URL=https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/16271/file?extension=JSON&encoding=UTF_8 ESNSI_OKOPF_CRON_LOAD=0 0 */1 * * * ESNSI_OKOPF_RETRY_MAX_ATTEMPTS_LOAD=3 -ESNSI_OKOPF_RETRY_DELAY_LOAD=30000 +ESNSI_OKOPF_RETRY_DELAY_LOAD=3000 +ESNSI_OKOPF_CONNECT_TIMEOUT=2 +ESNSI_OKOPF_READ_TIMEOUT=4 + +AUDIT_KAFKA_AUTHORIZATION_TOPIC=ervu.lkrp.auth.events +AUDIT_KAFKA_ACTION_TOPIC=ervu.lkrp.action.events +AUDIT_KAFKA_FILE_UPLOAD_TOPIC=ervu.lkrp.download.request +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 ERVU_FILE_UPLOAD_MAX_FILE_SIZE=5242880 ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456 diff --git a/config/standalone/dev/standalone.xml b/config/standalone/dev/standalone.xml index 7c97545e..74055d41 100644 --- a/config/standalone/dev/standalone.xml +++ b/config/standalone/dev/standalone.xml @@ -91,13 +91,24 @@ - + + + + + + + + + + + + diff --git a/config/tomcat/etc/tomcat/server.xml b/config/tomcat/etc/tomcat/server.xml index ec52cfa3..af21aff0 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 7c4532bd..00000000 --- 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 b1acab6e..00000000 --- a/frontend/home.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - -
- -
Реестр повесток юридических лиц
-
- -
-
-
-
-

- Доступ к личному кабинету для юридических лиц осуществляется исключительно через браузеры Яндекс или Chromium gost. -

-

- Пожалуйста, попробуйте снова, выбрав один из этих браузеров. -

-
-
-
-
-
-
-
Личный кабинет для ведения воинского учета в организациях
-
-
-
Кому доступен личный кабинет?
-
Организациям, за ĸоторыми заĸреплена обязанность по осуществлению воинсĸого учета в соответствии с ФЗ от 28.03.1998 No 53-ФЗ
-
-
-
Для чего нужен личный кабинет?
-
Для своевременной и оперативной передачи сведений в военĸоматы в элеĸтронном виде
-
-
-
-
-
-
-
-
Как получить доступ к Личному кабинету?
-
-
-
-
Необходимо авторизоваться
-
Потребуется подтвержденная учетная запись организации
-
Доступ предоставляется тольĸо сотрудниĸу, наделенному соответствующими полномочиями (ролью) на ведения воинсĸого учета внутри организации
-
- -
-
-
-
-
Какие виды сведений доступны для отправки?
-
-
-
-
- Сведения о приеме на работу (увольнении), зачислении в образовательную организацию (отчислении) -
Срок передачи сведений: до 5 дней
-
-
- Изменения сведений сотрудников, необходимых для ведения воинского учета -
Срок передачи сведений: до 5 дней
-
-
- Сообщение о гражданах, не состоящих, но обязанных состоять на воинском учете -
Срок передачи сведений: до 3 дней
-
-
-
-
-
-
- Ежегодное предоставление списка граждан мужского пола, подлежащих первоначальной постановке на воинский учет в год достижения ими возраста 17 лет -
Срок передачи сведений: ежегодно, в срок до 1 ноября
-
-
- Ежегодное предоставление списка сотрудников/обучающихся в организации, подлежащих воинскому учету -
Срок передачи сведений: ежегодно, по согласованию с военкоматом
-
-
-
-
-
-
-
Как передавать сведения?
-
-
Войдите в личный ĸабинет организации через Госуслуги
-
Подготовьте файл-csv с данными в соответствии с форматом в личном кабинете организации
-
Убедитесь, что все данные в файле введены ĸорреĸтно
-
Выберите необходимый вид сведений и загрузите файл
-
Следите за статусом приема
-
-
-
-
- Если в файле будут ошибĸи, данные не будут приняты Реестром и выгрузĸу сведений придется повторить -
-
-
-
-
-
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 535bde67..5b6686f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2462,9 +2462,9 @@ } }, "cadesplugin_api": { - "version": "2.0.4-micord.1", - "resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.0.4-micord.1.tgz", - "integrity": "sha512-FyGVi1VWIyJOW1zOOQN0IkTH/Z/8g7pNWH7A71nf0h21FCX9SacUfgRwID+gl+NlpYiT3m+yZGdlEJsiDeV8JA==" + "version": "2.1.1-micord.2", + "resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.1.1-micord.2.tgz", + "integrity": "sha512-+j8RfbL7t2YMlSOC9Oa6+NoNLMYC3ZHkc9W6JQnV5+NBUqKLPAlLL1DF6llmW8coRdmgH6nZU8skvdk6M6qaBg==" }, "calendar-utils": { "version": "0.8.5", diff --git a/frontend/package.json b/frontend/package.json index 9d69a21a..e0bec4bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "autonumeric": "4.5.10-cg", "bootstrap": "4.3.1", "bootstrap-icons": "1.10.3", - "cadesplugin_api": "2.0.4-micord.1", + "cadesplugin_api": "2.1.1-micord.2", "chart.js": "3.8.0-cg.1", "chartjs-adapter-moment": "1.0.0", "core-js": "2.4.1", diff --git a/frontend/src/resources/css/components-lkrp.css b/frontend/src/resources/css/components-lkrp.css index 96536517..f2a45121 100644 --- a/frontend/src/resources/css/components-lkrp.css +++ b/frontend/src/resources/css/components-lkrp.css @@ -606,7 +606,7 @@ } .webbpm.ervu_lkrp_ul div[page-filesentlog] { - height: calc(100% - var(--h-footer)); + flex: 1; } .webbpm.ervu_lkrp_ul #filesentlog, .webbpm.ervu_lkrp_ul .journal, @@ -941,4 +941,493 @@ } .webbpm.ervu_lkrp_ul #mydata .right-block field-set:first-child .fieldset::before { display: none; +} + +/*---------------- 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(--indentl--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); + 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 -------------*/ + +.browser-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 { + position: relative; + padding-left: 40px; +} +.browser-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; } \ No newline at end of file diff --git a/frontend/src/resources/css/inbox-lkrp.css b/frontend/src/resources/css/inbox-lkrp.css index 2e796b90..2404a462 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_ul { -ms-text-size-adjust: 100%; -moz-text-size-adjust: 100%; @@ -64,10 +83,18 @@ body.webbpm.ervu_lkrp_ul { --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_ul a { - color: var(--color-link); + color: var(--color-link); text-decoration: none; } .webbpm.ervu_lkrp_ul a:is(:hover, :focus, :active) { @@ -98,13 +125,16 @@ body.webbpm.ervu_lkrp_ul { background: url(../img/svg/logo.svg) no-repeat 0 50%; } .webbpm.ervu_lkrp_ul .header .header-logo .main-page { - font-family: 'InterSB'; + font-family: 'InterL'; margin-left: calc(62px + 16px); } .webbpm.ervu_lkrp_ul .header .header-logo .header-menu-left { font-family: 'InterL'; margin-left: 24px; } +.webbpm.ervu_lkrp_ul .header .header-logo .active { + font-family: 'InterSB'; +} .webbpm.ervu_lkrp_ul .header .header-menu { margin-right: var(--w-screen); } @@ -175,7 +205,7 @@ body.webbpm.ervu_lkrp_ul { top: auto; bottom: auto; left: auto; - right: auto; + right: auto; height: var(--h-footer); border: 0; background-color: var(--color-text-primary); @@ -186,8 +216,10 @@ body.webbpm.ervu_lkrp_ul { bottom: 0; } .webbpm.ervu_lkrp_ul .container-inside { + display: flex; + flex-direction: column; font-family: 'Inter'; - height: 100%; + height: 100%; padding: 0; overflow: auto; } @@ -205,7 +237,14 @@ body.webbpm.ervu_lkrp_ul { --indent-huge: 32px; --indent-big: 24px; --indent-medium: 24px; - --indent-small: 16px; + --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; } } @@ -218,7 +257,14 @@ body.webbpm.ervu_lkrp_ul { --indent-huge: 24px; --indent-big: 24px; --indent-medium: 16px; - --indent-small: 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_ul .header .header-logo .main-page { @@ -232,7 +278,7 @@ body.webbpm.ervu_lkrp_ul { text-overflow: ellipsis; max-width: 85px; overflow: hidden; - } + } } /*----------------- Login ---------------- */ diff --git a/frontend/src/resources/fonts/GolosText-Bold.ttf b/frontend/src/resources/fonts/GolosText-Bold.ttf new file mode 100644 index 00000000..3a41f9b5 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 00000000..5514593b 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 00000000..bb2c29b4 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 00000000..7e0f0e5b 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 00000000..088b8552 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 00000000..b7d08dcc --- /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 00000000..636c87e6 --- /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 00000000..aa8dc19c --- /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 00000000..322085c7 --- /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 00000000..d67a5b47 --- /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 00000000..1ab6dfd9 --- /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 00000000..66d332de --- /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 00000000..52c562c3 --- /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 00000000..d40c6e9e --- /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 00000000..04339c4a --- /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 00000000..121c26a3 --- /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 00000000..2ebaa04e --- /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 00000000..80fe8382 --- /dev/null +++ b/frontend/src/resources/img/svg/user-24x24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/resources/template/app/component/app_header.html b/frontend/src/resources/template/app/component/app_header.html index 010761f7..6e7d284c 100644 --- a/frontend/src/resources/template/app/component/app_header.html +++ b/frontend/src/resources/template/app/component/app_header.html @@ -1,10 +1,16 @@ -