diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/config/AuthProperties.java b/src/main/java/ru/copperside/config/AuthProperties.java
new file mode 100644
index 0000000..ae960eb
--- /dev/null
+++ b/src/main/java/ru/copperside/config/AuthProperties.java
@@ -0,0 +1,62 @@
+package ru.copperside.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "auth")
+public class AuthProperties {
+
+ private String endpoint = "/auth";
+
+ private Headers headers = new Headers();
+ private Response response = new Response();
+ private Logging logging = new Logging();
+ private HeaderForwarding headerForwarding = new HeaderForwarding();
+
+ private Set returnSessionPaths = new HashSet<>();
+
+ public boolean shouldReturnSessionForPath(String sourcePath) {
+ return returnSessionPaths.contains(sourcePath);
+ }
+
+ @Data
+ public static class Headers {
+ private String login;
+ private String signature;
+ private String originalPath;
+ }
+
+ @Data
+ public static class Response {
+ private String sessionFieldName = "SessionId";
+ }
+
+ @Data
+ public static class Logging {
+ private boolean logRequestBody = true;
+ private boolean logResponseBody = true;
+ private boolean logHeaders = true;
+ }
+
+ @Data
+ public static class HeaderForwarding {
+ private boolean enabled = true;
+ private Set exclude = new HashSet<>();
+
+ public boolean isExcluded(String headerName) {
+ if (headerName == null) return true;
+ String lower = headerName.toLowerCase(Locale.ROOT);
+
+ return exclude.stream()
+ .map(s -> s.toLowerCase(Locale.ROOT))
+ .anyMatch(lower::equals);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/controller/AuthController.java b/src/main/java/ru/copperside/controller/AuthController.java
new file mode 100644
index 0000000..a8cacdf
--- /dev/null
+++ b/src/main/java/ru/copperside/controller/AuthController.java
@@ -0,0 +1,112 @@
+package ru.copperside.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import ru.copperside.config.AuthProperties;
+import ru.copperside.logging.AuthLogger;
+import ru.copperside.service.AuthenticationService;
+
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping
+public class AuthController {
+
+ private final AuthenticationService authenticationService;
+ private final ObjectMapper objectMapper;
+ private final AuthProperties props;
+ private final AuthLogger authLogger;
+
+ @PostMapping(
+ path = "${auth.endpoint}",
+ consumes = "application/json",
+ produces = "application/json"
+ )
+ public ResponseEntity authenticate(
+ @RequestBody(required = false) JsonNode body,
+ HttpServletRequest request
+ ) {
+ // Собираем ВСЕ заголовки
+ Map headers = authLogger.extractHeaders(request);
+
+ // Пробуем сохранить бизнес-URI (X-Original-Path или что ты настроишь)
+ String originalPathHeaderName = props.getHeaders().getOriginalPath();
+ if (originalPathHeaderName != null) {
+ String originalPath = headers.get(originalPathHeaderName);
+ if (originalPath != null && !originalPath.isBlank()) {
+ request.setAttribute("ORIGINAL_PATH", originalPath);
+ }
+ }
+
+ // Логируем запрос (по конфигу)
+ authLogger.logRequest(request, body, headers);
+
+ // Валидация логина/подписи, прав и логика сессии — ТОЛЬКО в сервисе
+ authenticationService.authenticate(headers, body, request);
+
+ // Формируем тело ответа: по контракту возвращаем исходный body
+ JsonNode responseBody = body;
+
+ // При необходимости добавляем SessionId
+ if (Boolean.TRUE.equals(request.getAttribute("RETURN_SESSION"))) {
+ String sessionId = (String) request.getAttribute("SESSION_ID");
+ responseBody = addSessionToBody(body, sessionId);
+ }
+
+ // Пробрасываем заголовки назад (за исключением служебных)
+ HttpHeaders responseHeaders = buildForwardHeaders(headers);
+
+ // Логируем ответ (по конфигу)
+ authLogger.logResponse(request, responseBody);
+
+ return ResponseEntity.ok()
+ .headers(responseHeaders)
+ .body(responseBody);
+ }
+
+ private HttpHeaders buildForwardHeaders(Map headers) {
+ HttpHeaders resp = new HttpHeaders();
+ var cfg = props.getHeaderForwarding();
+
+ if (!cfg.isEnabled()) {
+ return resp;
+ }
+
+ headers.forEach((name, value) -> {
+ if (!cfg.isExcluded(name)) {
+ resp.add(name, value);
+ }
+ });
+
+ return resp;
+ }
+
+ private JsonNode addSessionToBody(JsonNode body, String sessionId) {
+ String field = props.getResponse().getSessionFieldName();
+
+ if (body == null || body.isNull()) {
+ ObjectNode node = objectMapper.createObjectNode();
+ node.put(field, sessionId);
+ return node;
+ }
+
+ if (body.isObject()) {
+ ((ObjectNode) body).put(field, sessionId);
+ return body;
+ }
+
+ ObjectNode wrapper = objectMapper.createObjectNode();
+ wrapper.set("data", body);
+ wrapper.put(field, sessionId);
+ return wrapper;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/exception/GlobalExceptionHandler.java b/src/main/java/ru/copperside/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..d470d42
--- /dev/null
+++ b/src/main/java/ru/copperside/exception/GlobalExceptionHandler.java
@@ -0,0 +1,97 @@
+package ru.copperside.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import ru.copperside.exception.dto.ErrorResponseDto;
+import ru.copperside.logging.AuthLogger;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@RestControllerAdvice
+@RequiredArgsConstructor
+public class GlobalExceptionHandler {
+
+ private static final String EXCEPTION_TYPE = "Error";
+
+ private final AuthLogger authLogger;
+
+ @ExceptionHandler(InvalidUserNameOrSignatureException.class)
+ public ResponseEntity handleInvalidUserNameOrSignature(
+ InvalidUserNameOrSignatureException ex,
+ HttpServletRequest request
+ ) {
+ String sourcePath = resolveSourcePath(request);
+ String sessionId = resolveSessionId(request);
+
+ ErrorResponseDto body = ErrorResponseDto.builder()
+ .ExceptionType(EXCEPTION_TYPE)
+ .Code("InvalidUserNameOrSignature")
+ .Message("InvalidUserNameOrSignature")
+ .AdapterName("")
+ .SourceType("")
+ .SourcePath(sourcePath)
+ .SessionId(sessionId.isEmpty() ? null : sessionId)
+ .TimeStamp(OffsetDateTime.now())
+ .Properties(Map.of("Cause", "Данные не соответствуют подписи"))
+ .build();
+
+ int status = HttpStatus.UNAUTHORIZED.value();
+
+ // логируем в структурированный лог
+ authLogger.logError(request, body, status);
+
+ // при желании оставляем и "человеческий" лог
+ log.warn("InvalidUserNameOrSignature: {}", ex.getMessage());
+
+ return ResponseEntity.status(status).body(body);
+ }
+
+ @ExceptionHandler(InsufficientPermissionException.class)
+ public ResponseEntity handleInsufficientPermission(
+ InsufficientPermissionException ex,
+ HttpServletRequest request
+ ) {
+ String sourcePath = resolveSourcePath(request);
+ String sessionId = resolveSessionId(request);
+
+ ErrorResponseDto body = ErrorResponseDto.builder()
+ .ExceptionType(EXCEPTION_TYPE)
+ .Code("InsufficientPermission")
+ .Message("")
+ .AdapterName("")
+ .SourceType("")
+ .SourcePath(sourcePath)
+ .SessionId(sessionId.isEmpty() ? "" : sessionId)
+ .TimeStamp(OffsetDateTime.now())
+ .build();
+
+ int status = HttpStatus.FORBIDDEN.value();
+
+ authLogger.logError(request, body, status);
+ log.warn("InsufficientPermission: {}", ex.getMessage());
+
+ return ResponseEntity.status(status).body(body);
+ }
+
+ private String resolveSourcePath(HttpServletRequest request) {
+ Object attr = request.getAttribute("ORIGINAL_PATH");
+ if (attr instanceof String s && !s.isBlank()) {
+ return s;
+ }
+ return Optional.ofNullable(request.getRequestURI()).orElse("");
+ }
+
+ private String resolveSessionId(HttpServletRequest request) {
+ return Optional.ofNullable(request.getSession(false))
+ .map(s -> s.getId())
+ .orElse("");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/exception/InsufficientPermissionException.java b/src/main/java/ru/copperside/exception/InsufficientPermissionException.java
new file mode 100644
index 0000000..2824e5e
--- /dev/null
+++ b/src/main/java/ru/copperside/exception/InsufficientPermissionException.java
@@ -0,0 +1,10 @@
+package ru.copperside.exception;
+
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class InsufficientPermissionException extends RuntimeException {
+ public InsufficientPermissionException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/exception/InvalidUserNameOrSignatureException.java b/src/main/java/ru/copperside/exception/InvalidUserNameOrSignatureException.java
new file mode 100644
index 0000000..bb6c08c
--- /dev/null
+++ b/src/main/java/ru/copperside/exception/InvalidUserNameOrSignatureException.java
@@ -0,0 +1,10 @@
+package ru.copperside.exception;
+
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class InvalidUserNameOrSignatureException extends RuntimeException {
+ public InvalidUserNameOrSignatureException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/exception/dto/ErrorResponseDto.java b/src/main/java/ru/copperside/exception/dto/ErrorResponseDto.java
new file mode 100644
index 0000000..48ae0a7
--- /dev/null
+++ b/src/main/java/ru/copperside/exception/dto/ErrorResponseDto.java
@@ -0,0 +1,24 @@
+package ru.copperside.exception.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+@Data
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ErrorResponseDto {
+
+ private String ExceptionType;
+ private String Code;
+ private String Message;
+ private String AdapterName;
+ private String SourceType;
+ private String SourcePath;
+ private String SessionId;
+ private OffsetDateTime TimeStamp;
+ private Map Properties;
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/logging/AuthLogger.java b/src/main/java/ru/copperside/logging/AuthLogger.java
new file mode 100644
index 0000000..3b4c926
--- /dev/null
+++ b/src/main/java/ru/copperside/logging/AuthLogger.java
@@ -0,0 +1,92 @@
+package ru.copperside.logging;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import ru.copperside.config.AuthProperties;
+import ru.copperside.exception.dto.ErrorResponseDto;
+
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AuthLogger {
+
+ private final AuthProperties props;
+ private final ObjectMapper mapper;
+
+ public void logRequest(HttpServletRequest request,
+ JsonNode body,
+ Map headers) {
+ var cfg = props.getLogging();
+ if (!cfg.isLogHeaders() && !cfg.isLogRequestBody()) return;
+
+ try {
+ Map event = new LinkedHashMap<>();
+ event.put("event", "auth_request");
+ event.put("method", request.getMethod());
+ event.put("path", request.getRequestURI());
+ event.put("remoteAddr", request.getRemoteAddr());
+
+ if (cfg.isLogHeaders()) event.put("headers", headers);
+ if (cfg.isLogRequestBody()) event.put("body", body);
+
+ log.info(mapper.writeValueAsString(event));
+ } catch (Exception e) {
+ log.warn("Failed to log request", e);
+ }
+ }
+
+ public void logResponse(HttpServletRequest request, JsonNode body) {
+ var cfg = props.getLogging();
+ if (!cfg.isLogResponseBody()) return;
+
+ try {
+ Map event = new LinkedHashMap<>();
+ event.put("event", "auth_response");
+ event.put("path", request.getRequestURI());
+ event.put("status", 200);
+ event.put("body", body);
+
+ log.info(mapper.writeValueAsString(event));
+ } catch (Exception e) {
+ log.warn("Failed to log response", e);
+ }
+ }
+
+ /**
+ * Логирование ОШИБОЧНОГО ответа.
+ * Логируем всегда, независимо от флагов (ошибки терять нельзя).
+ */
+ public void logError(HttpServletRequest request,
+ ErrorResponseDto errorBody,
+ int status) {
+ try {
+ Map event = new LinkedHashMap<>();
+ event.put("event", "auth_error");
+ event.put("path", request.getRequestURI());
+ event.put("status", status);
+ event.put("error", errorBody);
+
+ log.warn(mapper.writeValueAsString(event));
+ } catch (Exception e) {
+ log.warn("Failed to log error response", e);
+ }
+ }
+
+ public Map extractHeaders(HttpServletRequest request) {
+ Map map = new LinkedHashMap<>();
+ Enumeration names = request.getHeaderNames();
+ while (names != null && names.hasMoreElements()) {
+ String n = names.nextElement();
+ map.put(n, request.getHeader(n));
+ }
+ return map;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/model/authinfo/AuthInfo.java b/src/main/java/ru/copperside/model/authinfo/AuthInfo.java
index a2b56a4..8a813de 100644
--- a/src/main/java/ru/copperside/model/authinfo/AuthInfo.java
+++ b/src/main/java/ru/copperside/model/authinfo/AuthInfo.java
@@ -13,12 +13,12 @@ import java.util.List;
public record AuthInfo(
Long authId,
String dataId,
+ Long merchId,
Long hierarchyId,
String displayName,
Boolean isEnabled,
Boolean needActivation,
List permissions,
- SessionSettings sessionSettings,
JsonNode sessionData,
JsonNode privateData,
SecretData secretData
diff --git a/src/main/java/ru/copperside/service/AuthenticationService.java b/src/main/java/ru/copperside/service/AuthenticationService.java
new file mode 100644
index 0000000..37fb193
--- /dev/null
+++ b/src/main/java/ru/copperside/service/AuthenticationService.java
@@ -0,0 +1,15 @@
+package ru.copperside.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.Map;
+
+public interface AuthenticationService {
+
+ void authenticate(
+ Map headers,
+ JsonNode body,
+ HttpServletRequest request
+ );
+}
\ No newline at end of file
diff --git a/src/main/java/ru/copperside/service/AuthenticationServiceImpl.java b/src/main/java/ru/copperside/service/AuthenticationServiceImpl.java
new file mode 100644
index 0000000..e124f3f
--- /dev/null
+++ b/src/main/java/ru/copperside/service/AuthenticationServiceImpl.java
@@ -0,0 +1,81 @@
+package ru.copperside.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.copperside.service.AuthenticationService;
+import ru.copperside.config.AuthProperties;
+import ru.copperside.exception.InsufficientPermissionException;
+import ru.copperside.exception.InvalidUserNameOrSignatureException;
+
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AuthenticationServiceImpl implements AuthenticationService {
+
+ private final AuthProperties props;
+
+ @Override
+ public void authenticate(Map headers,
+ JsonNode body,
+ HttpServletRequest request) {
+
+ // 1. Достаём логин и подпись из заголовков
+ String loginHeaderName = props.getHeaders().getLogin();
+ String signatureHeaderName = props.getHeaders().getSignature();
+
+ String login = headers.get(loginHeaderName);
+ String signature = headers.get(signatureHeaderName);
+
+ if (isBlank(login) || isBlank(signature)) {
+ // Отсутствие нужных заголовков — это та же ошибка InvalidUserNameOrSignature
+ throw new InvalidUserNameOrSignatureException("Missing login or signature header");
+ }
+
+ // 2. Проверка подписи (заглушка; здесь можешь использовать login/signature/body/headers)
+ boolean signatureOk = true; // TODO: реальная проверка HMAC
+ if (!signatureOk) {
+ throw new InvalidUserNameOrSignatureException("Invalid signature");
+ }
+
+ // 3. Проверка прав (заглушка; тут уже знаешь authId/permissions и т.д.)
+ boolean hasPerm = true; // TODO: реальная проверка прав
+ if (!hasPerm) {
+ throw new InsufficientPermissionException("Insufficient permissions");
+ }
+
+ // 4. Работа с SessionId при необходимости
+ String sourcePath = resolveSourcePath(request);
+
+ if (props.shouldReturnSessionForPath(sourcePath)) {
+ HttpSession session = request.getSession(true);
+ request.setAttribute("RETURN_SESSION", Boolean.TRUE);
+ request.setAttribute("SESSION_ID", session.getId());
+ }
+ }
+
+ private String resolveSourcePath(HttpServletRequest req) {
+ Object attr = req.getAttribute("ORIGINAL_PATH");
+ if (attr instanceof String s && !s.isBlank()) {
+ return s;
+ }
+
+ String headerName = props.getHeaders().getOriginalPath();
+ String header = headerName != null ? req.getHeader(headerName) : null;
+ if (header != null && !header.isBlank()) {
+ return header;
+ }
+
+ return Optional.ofNullable(req.getRequestURI()).orElse("");
+ }
+
+ private boolean isBlank(String s) {
+ return s == null || s.trim().isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index c4595cb..03ca778 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -7,3 +7,37 @@ spring:
username: AUTH
password: .,z;pws-2shds
driver-class-name: oracle.jdbc.OracleDriver
+
+auth:
+ endpoint: /auth
+
+ headers:
+ login: TCB-Header-Login
+ signature: TCB-Header-Sign
+ original-path: X-Original-Path
+
+ response:
+ session-field-name: SessionId
+
+ return-session-paths:
+ - /api/v1/order/state
+
+ logging:
+ log-request-body: true
+ log-response-body: true
+ log-headers: true
+
+ header-forwarding:
+ enabled: true
+ # Заголовки, которые НЕ нужно возвращать (регистр игнорируем)
+ exclude:
+ - content-length
+ - host
+ - connection
+ - transfer-encoding
+ - keep-alive
+ - proxy-authenticate
+ - proxy-authorization
+ - te
+ - trailer
+ - upgrade
\ No newline at end of file