From bbc4d3728e6938f0128a276c111e22cb49c15eac Mon Sep 17 00:00:00 2001 From: 250 Date: Sat, 4 Apr 2026 13:52:08 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat(AI):=20AI=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B0=8F=20Slack/AI=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EC=9D=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ai/AiGenerator.java | 9 ++ .../notificationservice/domain/ai/AiLog.java | 99 +++++++++++++++++++ .../domain/ai/exception/AiErrorCode.java | 49 +++++++++ .../domain/ai/repository/AiLogRepository.java | 18 ++++ .../domain/ai/type/AiRequestStatus.java | 7 ++ .../domain/ai/type/AiRequestType.java | 7 ++ .../domain/ai/vo/AiRequestInfo.java | 23 +++++ .../domain/ai/vo/AiResponseInfo.java | 8 ++ .../domain/slack/SlackMessage.java | 43 +------- 9 files changed, 223 insertions(+), 40 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestStatus.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestType.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java new file mode 100644 index 0000000..6993595 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java @@ -0,0 +1,9 @@ +package com.shipflow.notificationservice.domain.ai; + +import com.shipflow.notificationservice.domain.ai.vo.AiRequestInfo; +import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; + +public interface AiGenerator { + + AiResponseInfo generate(AiRequestInfo requestInfo); +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java new file mode 100644 index 0000000..cd95aa5 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java @@ -0,0 +1,99 @@ +package com.shipflow.notificationservice.domain.ai; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.common.domain.BaseEntity; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "p_ai_log", schema = "notification") +public class AiLog extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "related_shipment_id") + private UUID relatedShipmentId; + + @Column(name = "shipment_manager_id") + private UUID shipmentManagerId; + + @Column(name = "prompt", columnDefinition = "TEXT", nullable = false) + private String prompt; + + @Column(name = "response_text", columnDefinition = "TEXT") + private String responseText; + + @Column(name = "final_deadline_at") + private LocalDateTime finalDeadlineAt; + + @Column(name = "work_date") + private LocalDate workDate; + @Enumerated(EnumType.STRING) + @Column(name = "send_status", length = 20, nullable = false) + private SlackSendStatus sendStatus; + + @Enumerated(EnumType.STRING) + @Column(name = "request_type", length = 20, nullable = false) + private AiRequestType requestType; + + @Enumerated(EnumType.STRING) + @Column(name = "request_status", length = 20, nullable = false) + private AiRequestStatus requestStatus; + + protected AiLog() { + } + + public AiLog(UUID relatedShipmentId, + UUID shipmentManagerId, + String prompt, + AiRequestType requestType) { + + this.relatedShipmentId = relatedShipmentId; + this.shipmentManagerId = shipmentManagerId; + this.prompt = prompt; + this.requestType = requestType; + + this.requestStatus = AiRequestStatus.PENDING; + this.sendStatus = SlackSendStatus.PENDING; + } + + //AI 성공 + public void markSuccess(String responseText, LocalDateTime finalDeadlineAt) { + this.responseText = responseText; + this.finalDeadlineAt = finalDeadlineAt; + this.requestStatus = AiRequestStatus.SUCCESS; + } + + //AI 실패 + public void markFail() { + this.requestStatus = AiRequestStatus.FAIL; + } + + //슬랙 발송 성공 + public void markSendSuccess() { + this.sendStatus = SlackSendStatus.SUCCESS; + } + + // 슬랙 발송 실패 + public void markSendFail() { + this.sendStatus = SlackSendStatus.FAIL; + } + +} //끝 diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java new file mode 100644 index 0000000..f4edd5b --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java @@ -0,0 +1,49 @@ +package com.shipflow.notificationservice.domain.ai.exception; + +import org.springframework.http.HttpStatus; + +import com.shipflow.common.exception.ErrorCode; + +public enum AiErrorCode implements ErrorCode { + + // 조회 + AI_LOG_NOT_FOUND("AI_LOG_NOT_FOUND", HttpStatus.NOT_FOUND, "AI 로그를 찾을 수 없습니다."), + + // 이벤트 + AI_EVENT_NOT_FOUND("AI_EVENT_NOT_FOUND", HttpStatus.BAD_REQUEST, "AI 이벤트를 찾을 수 없습니다."), + AI_EVENT_INVALID("AI_EVENT_INVALID", HttpStatus.BAD_REQUEST, "유효하지 않은 AI 이벤트입니다."), + + // 요청값 검증 + AI_REQUEST_TYPE_REQUIRED("AI_REQUEST_TYPE_REQUIRED", HttpStatus.BAD_REQUEST, "AI 요청 타입은 필수입니다."), + AI_PROMPT_REQUIRED("AI_PROMPT_REQUIRED", HttpStatus.BAD_REQUEST, "AI 프롬프트는 필수입니다."), + + // 생성 (Gemini 호출) + AI_GENERATE_FAILED("AI_GENERATE_FAILED", HttpStatus.INTERNAL_SERVER_ERROR, "AI 생성에 실패했습니다."), + AI_RESPONSE_EMPTY("AI_RESPONSE_EMPTY", HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답이 비어있습니다."), + AI_RESPONSE_PARSE_FAILED("AI_RESPONSE_PARSE_FAILED", HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 파싱에 실패했습니다."); + + private final String code; + private final HttpStatus status; + private final String message; + + AiErrorCode(String code, HttpStatus status, String message) { + this.code = code; + this.status = status; + this.message = message; + } + + @Override + public String code() { + return code; + } + + @Override + public HttpStatus status() { + return status; + } + + @Override + public String message() { + return message; + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java new file mode 100644 index 0000000..a33f564 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java @@ -0,0 +1,18 @@ +package com.shipflow.notificationservice.domain.ai.repository; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.shipflow.notificationservice.domain.ai.AiLog; + +public interface AiLogRepository { + + AiLog save(AiLog aiLog); + + Optional findByIdAndDeletedAtIsNull(UUID id); + + Page findAllByDeletedAtIsNull(Pageable pageable); +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestStatus.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestStatus.java new file mode 100644 index 0000000..1e74292 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestStatus.java @@ -0,0 +1,7 @@ +package com.shipflow.notificationservice.domain.ai.type; + +public enum AiRequestStatus { + SUCCESS, + FAIL, + PENDING +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestType.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestType.java new file mode 100644 index 0000000..922e3b8 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/type/AiRequestType.java @@ -0,0 +1,7 @@ +package com.shipflow.notificationservice.domain.ai.type; + +public enum AiRequestType { + DEADLINE, + ROUTE_ORDER, + ROUTE_MESSAGE +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java new file mode 100644 index 0000000..cca4353 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java @@ -0,0 +1,23 @@ +package com.shipflow.notificationservice.domain.ai.vo; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; + +public class AiRequestInfo { + //이벤트 + private String formHub; + private String toHub; + private List route; + private String product; + private String requestNote; + private LocalDateTime deadline; + private String workingHours; + + private AiRequestType requestType; + //도전기능 + private LocalDate workDate; + +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java new file mode 100644 index 0000000..294a9ba --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java @@ -0,0 +1,8 @@ +package com.shipflow.notificationservice.domain.ai.vo; + +import java.time.LocalDateTime; + +public class AiResponseInfo { + private String responseText; + private LocalDateTime finalDeadlineAt; +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java index 65a631f..64e99d3 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java @@ -17,7 +17,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.Getter; +@Getter @Entity @Table(name = "p_slack", schema = "notification") public class SlackMessage extends BaseEntity { @@ -124,44 +126,5 @@ public void validateDeletable() { public void markFail() { this.sendStatus = SlackSendStatus.FAIL; } - - public UUID getId() { - return id; - } - - public String getReceiverSlackId() { - return receiverSlackId; - } - - public UUID getRelatedShipmentId() { - return relatedShipmentId; - } - - public UUID getRelatedAiLogId() { - return relatedAiLogId; - } - - public String getSlackTs() { - return slackTs; - } - - public String getSlackChannelId() { - return slackChannelId; - } - - public String getMessage() { - return message; - } - - public SlackMessageType getMessageType() { - return messageType; - } - - public SlackSendStatus getSendStatus() { - return sendStatus; - } - - public LocalDateTime getSentAt() { - return sentAt; - } + } From a6d85a0de45f3461fb38141d84a2c60cc6359a4c Mon Sep 17 00:00:00 2001 From: 250 Date: Sat, 4 Apr 2026 14:15:25 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat(AI):=20AI=20=EB=A1=9C=EA=B7=B8=20Per?= =?UTF-8?q?sistence=20=EA=B3=84=EC=B8=B5=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ai/AiLogJpaRepository.java | 18 ++++++++++ .../persistence/ai/AiLogRepositoryImpl.java | 36 +++++++++++++++++++ .../presentation/common/BasePageRequest.java | 22 ++++++++++++ .../presentation/common/BasePageResponse.java | 27 ++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageResponse.java diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java new file mode 100644 index 0000000..fbd1260 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java @@ -0,0 +1,18 @@ +package com.shipflow.notificationservice.infrastructure.persistence.ai; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.shipflow.notificationservice.domain.ai.AiLog; + +public interface AiLogJpaRepository extends JpaRepository { + + Optional findByIdAndDeletedAtIsNull(UUID id); + + Page findAllByDeletedAtIsNull(Pageable pageable); + +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java new file mode 100644 index 0000000..57895b8 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.shipflow.notificationservice.infrastructure.persistence.ai; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AiLogRepositoryImpl implements AiLogRepository { + + private final AiLogJpaRepository aiLogJpaRepository; + + @Override + public AiLog save(AiLog aiLog) { + return aiLogJpaRepository.save(aiLog); + } + + @Override + public Optional findByIdAndDeletedAtIsNull(UUID aiId) { + return aiLogJpaRepository.findByIdAndDeletedAtIsNull(aiId); + } + + @Override + public Page findAllByDeletedAtIsNull(Pageable pageable) { + return aiLogJpaRepository.findAllByDeletedAtIsNull(pageable); + } +} + diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java new file mode 100644 index 0000000..c735b4b --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java @@ -0,0 +1,22 @@ +package com.shipflow.notificationservice.presentation.common; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public record BasePageRequest( + int page, + int size +) { + public BasePageRequest { + page = Math.max(page, 0); + + if (size != 10 && size != 30 && size != 50) { + size = 10; + } + } + + public Pageable toPageable(Sort sort) { + return PageRequest.of(page, size, sort); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageResponse.java new file mode 100644 index 0000000..6ac93d6 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageResponse.java @@ -0,0 +1,27 @@ +package com.shipflow.notificationservice.presentation.common; + +import java.util.List; + +import org.springframework.data.domain.Page; + +public record BasePageResponse( + List content, + int page, + int size, + long totalElements, + int totalPages, + boolean first, + boolean last +) { + public static BasePageResponse from(Page page) { + return new BasePageResponse<>( + page.getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isFirst(), + page.isLast() + ); + } +} \ No newline at end of file From 8c0fb9b52151748e4e9e188e50a5d2844c838ad0 Mon Sep 17 00:00:00 2001 From: 250 Date: Sat, 4 Apr 2026 23:34:11 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat(AI):=20Gemini=20AI=20Client=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20AiGenerator=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notification-service/build.gradle | 26 ++- .../notificationservice/domain/ai/AiLog.java | 1 + .../domain/ai/exception/AiErrorCode.java | 9 +- .../domain/ai/vo/AiRequestInfo.java | 23 ++- .../domain/ai/vo/AiResponseInfo.java | 9 +- .../client/ai/GeminiApiClient.java | 170 ++++++++++++++++++ .../client/ai/config/GeminiApiConfig.java | 15 ++ .../client/ai/config/GeminiProperties.java | 22 +++ .../client/ai/dto/GeminiRequest.java | 32 ++++ .../client/ai/dto/GeminiResponse.java | 26 +++ .../src/main/resources/application.yaml | 9 +- 11 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiProperties.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiResponse.java diff --git a/notification-service/build.gradle b/notification-service/build.gradle index 939fe81..f3b776e 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -24,21 +24,37 @@ repositories { } dependencies { + + // 1. Web / External API implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - runtimeOnly 'org.postgresql:postgresql' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // 2. Persistence (DB) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'org.postgresql:postgresql' + + // 3. External Services implementation 'com.slack.api:slack-api-client:1.45.3' + + // 4. QueryDSL implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + + // 5. Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // 6. Common Module implementation project(':common') + // 7. Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // 8. Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java index cd95aa5..f0b5df8 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java @@ -45,6 +45,7 @@ public class AiLog extends BaseEntity { @Column(name = "work_date") private LocalDate workDate; + @Enumerated(EnumType.STRING) @Column(name = "send_status", length = 20, nullable = false) private SlackSendStatus sendStatus; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java index f4edd5b..4c0f573 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java @@ -12,10 +12,13 @@ public enum AiErrorCode implements ErrorCode { // 이벤트 AI_EVENT_NOT_FOUND("AI_EVENT_NOT_FOUND", HttpStatus.BAD_REQUEST, "AI 이벤트를 찾을 수 없습니다."), AI_EVENT_INVALID("AI_EVENT_INVALID", HttpStatus.BAD_REQUEST, "유효하지 않은 AI 이벤트입니다."), - - // 요청값 검증 AI_REQUEST_TYPE_REQUIRED("AI_REQUEST_TYPE_REQUIRED", HttpStatus.BAD_REQUEST, "AI 요청 타입은 필수입니다."), - AI_PROMPT_REQUIRED("AI_PROMPT_REQUIRED", HttpStatus.BAD_REQUEST, "AI 프롬프트는 필수입니다."), + + // 필수 데이터 + AI_FROM_HUB_REQUIRED("AI_FROM_HUB_REQUIRED", HttpStatus.BAD_REQUEST, "출발 허브 정보는 필수입니다."), + AI_TO_HUB_REQUIRED("AI_TO_HUB_REQUIRED", HttpStatus.BAD_REQUEST, "도착 허브 정보는 필수입니다."), + AI_PRODUCT_REQUIRED("AI_PRODUCT_REQUIRED", HttpStatus.BAD_REQUEST, "상품 정보는 필수입니다."), + AI_DEADLINE_REQUIRED("AI_DEADLINE_REQUIRED", HttpStatus.BAD_REQUEST, "납기 정보는 필수입니다."), // 생성 (Gemini 호출) AI_GENERATE_FAILED("AI_GENERATE_FAILED", HttpStatus.INTERNAL_SERVER_ERROR, "AI 생성에 실패했습니다."), diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java index cca4353..cbfded5 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java @@ -6,18 +6,17 @@ import com.shipflow.notificationservice.domain.ai.type.AiRequestType; -public class AiRequestInfo { +public record AiRequestInfo( //이벤트 - private String formHub; - private String toHub; - private List route; - private String product; - private String requestNote; - private LocalDateTime deadline; - private String workingHours; - - private AiRequestType requestType; + String fromHub, + String toHub, + List route, + String product, + String requestNote, + LocalDateTime deadline, + String workingHours, + AiRequestType requestType, //도전기능 - private LocalDate workDate; - + LocalDate workDate +) { } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java index 294a9ba..46bccd5 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiResponseInfo.java @@ -2,7 +2,8 @@ import java.time.LocalDateTime; -public class AiResponseInfo { - private String responseText; - private LocalDateTime finalDeadlineAt; -} +public record AiResponseInfo( + String responseText, + LocalDateTime finalDeadlineAt +) { +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java new file mode 100644 index 0000000..340e967 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java @@ -0,0 +1,170 @@ +package com.shipflow.notificationservice.infrastructure.client.ai; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.domain.ai.AiGenerator; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.vo.AiRequestInfo; +import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; +import com.shipflow.notificationservice.infrastructure.client.ai.config.GeminiProperties; +import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiRequest; +import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiResponse; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Component +public class GeminiApiClient implements AiGenerator { + + private static final String API_KEY_HEADER = "x-goog-api-key"; + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + private static final Pattern DEADLINE_PATTERN = + Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2})?"); + private static final String WORKING_HOURS = "09:00 ~ 18:00"; + + private final WebClient webClient; + private final GeminiProperties geminiProperties; + + @Override + public AiResponseInfo generate(AiRequestInfo aiRequestInfo) { + validateRequestInfo(aiRequestInfo); + GeminiResponse response; + String prompt = createPrompt(aiRequestInfo); + + try { + response = webClient.post() + .uri(geminiProperties.getUrl()) + .header(API_KEY_HEADER, geminiProperties.getApiKey()) + .bodyValue(new GeminiRequest(prompt)) + .retrieve() + + // 4xx 에러 처리 (잘못된 요청) + .onStatus( + status -> status.is4xxClientError(), + res -> Mono.error(new BusinessException(AiErrorCode.AI_GENERATE_FAILED)) + ) + + // 5xx 에러 처리 (서버 오류) + .onStatus( + status -> status.is5xxServerError(), + res -> Mono.error(new BusinessException(AiErrorCode.AI_GENERATE_FAILED)) + ) + + .bodyToMono(GeminiResponse.class) + .timeout(REQUEST_TIMEOUT) + .block(); + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + throw new BusinessException(AiErrorCode.AI_GENERATE_FAILED); + } + + String text = extractText(response); + LocalDateTime finalDeadlineAt = extractDeadline(text); + + return new AiResponseInfo(text, finalDeadlineAt); + } + + //프롬포트 + private String createPrompt(AiRequestInfo info) { + String routeText = (info.route() == null || info.route().isEmpty()) + ? "없음" + : String.join(", ", info.route()); + String requestNote = (info.requestNote() == null || info.requestNote().isBlank()) + ? "없음" + : info.requestNote(); + + return """ + 다음 물류 정보를 바탕으로 최종 발송 시한을 계산해라. + + 발송지: %s + 경유지: %s + 도착지: %s + 상품: %s + 요청사항: %s + 납기: %s + 근무시간: %s + + 반드시 ISO-8601 형식의 발송 시한만 포함해서 응답해라. + 예시: 2026-04-04T09:00:00 + """.formatted( + info.fromHub(), + routeText, + info.toHub(), + info.product(), + info.requestNote(), + info.deadline(), + WORKING_HOURS + ); + } + + //유효성 검증 + private void validateRequestInfo(AiRequestInfo aiRequestInfo) { + if (aiRequestInfo == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_NOT_FOUND); + } + if (aiRequestInfo.requestType() == null) { + throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); + } + if (aiRequestInfo.fromHub() == null || aiRequestInfo.fromHub().isBlank()) { + throw new BusinessException(AiErrorCode.AI_FROM_HUB_REQUIRED); + } + + if (aiRequestInfo.toHub() == null || aiRequestInfo.toHub().isBlank()) { + throw new BusinessException(AiErrorCode.AI_TO_HUB_REQUIRED); + } + + if (aiRequestInfo.product() == null || aiRequestInfo.product().isBlank()) { + throw new BusinessException(AiErrorCode.AI_PRODUCT_REQUIRED); + } + + if (aiRequestInfo.deadline() == null) { + throw new BusinessException(AiErrorCode.AI_DEADLINE_REQUIRED); + } + + } + + private String extractText(GeminiResponse response) { + if (response == null + || response.getCandidates() == null + || response.getCandidates().isEmpty() + || response.getCandidates().get(0).getContent() == null + || response.getCandidates().get(0).getContent().getParts() == null + || response.getCandidates().get(0).getContent().getParts().isEmpty() + || response.getCandidates().get(0).getContent().getParts().get(0).getText() == null + || response.getCandidates().get(0).getContent().getParts().get(0).getText().isBlank()) { + throw new BusinessException(AiErrorCode.AI_RESPONSE_EMPTY); + } + + return response.getCandidates() + .get(0) + .getContent() + .getParts() + .get(0) + .getText() + .trim(); + } + + private LocalDateTime extractDeadline(String text) { + Matcher matcher = DEADLINE_PATTERN.matcher(text); + if (!matcher.find()) { + throw new BusinessException(AiErrorCode.AI_RESPONSE_PARSE_FAILED); + } + + try { + return LocalDateTime.parse(matcher.group()); + } catch (DateTimeParseException e) { + throw new BusinessException(AiErrorCode.AI_RESPONSE_PARSE_FAILED); + } + } + +}//끝 \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java new file mode 100644 index 0000000..18f2cab --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java @@ -0,0 +1,15 @@ +package com.shipflow.notificationservice.infrastructure.client.ai.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@EnableConfigurationProperties(GeminiProperties.class) +public class GeminiApiConfig { + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiProperties.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiProperties.java new file mode 100644 index 0000000..193393e --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiProperties.java @@ -0,0 +1,22 @@ +package com.shipflow.notificationservice.infrastructure.client.ai.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "gemini") +public class GeminiProperties { + + @NotNull + private String apiKey; + @NotNull + private String url; + +} + diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java new file mode 100644 index 0000000..58cd0b5 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java @@ -0,0 +1,32 @@ +package com.shipflow.notificationservice.infrastructure.client.ai.dto; + +import java.util.List; + +import lombok.Getter; + +public class GeminiRequest { + + private List contents; + + public GeminiRequest(String prompt) { + this.contents = List.of(new Content(prompt)); + } + + @Getter + public static class Content { + private List parts; + + public Content(String text) { + this.parts = List.of(new Part(text)); + } + } + + @Getter + public static class Part { + private String text; + + public Part(String text) { + this.text = text; + } + } +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiResponse.java new file mode 100644 index 0000000..4005f90 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiResponse.java @@ -0,0 +1,26 @@ +package com.shipflow.notificationservice.infrastructure.client.ai.dto; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class GeminiResponse { + + private List candidates; + + @Getter + public static class Candidate { + private Content content; + } + + @Getter + public static class Content { + private List parts; + } + + @Getter + public static class Part { + private String text; + } +} \ No newline at end of file diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index 1feda3b..826ed52 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -13,5 +13,12 @@ spring: hibernate: default_schema: notification + config: + import: optional:file:.env + slack: - bot-token: ${SLACK_BOT_TOKEN} \ No newline at end of file + bot-token: ${SLACK_BOT_TOKEN} + +gemini: + api-key: ${GEMINI_API_KEY} + url: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent \ No newline at end of file From 712408a5f022fb434c325c3b4b6304ebac9c939c Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 00:38:23 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat(AI):=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20AI=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=EC=83=9D=EC=84=B1,=20=EB=8B=A8=EA=B1=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C)=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ai/AiAppService.java | 139 ++++++++++++++++++ .../dto/command/GenerateDeadlineCommand.java} | 9 +- .../ai/dto/result/AiLogResult.java | 38 +++++ .../domain/ai/AiGenerator.java | 6 +- .../domain/ai/exception/AiErrorCode.java | 1 + .../client/ai/GeminiApiClient.java | 77 +--------- .../dto/request/GenerateDeadlineRequest.java | 61 ++++++++ .../ai/dto/response/AiLogResponse.java | 39 +++++ .../ai/external/AiController.java | 58 ++++++++ 9 files changed, 351 insertions(+), 77 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java rename notification-service/src/main/java/com/shipflow/notificationservice/{domain/ai/vo/AiRequestInfo.java => application/ai/dto/command/GenerateDeadlineCommand.java} (65%) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/result/AiLogResult.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/response/AiLogResponse.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java new file mode 100644 index 0000000..0d7fd9b --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -0,0 +1,139 @@ +package com.shipflow.notificationservice.application.ai; + +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.domain.ai.AiGenerator; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class AiAppService { + + private static final String DEFAULT_WORKING_HOURS = "09:00 ~ 18:00"; + + private final AiLogRepository aiLogRepository; + private final AiGenerator aiGenerator; + + @Transactional + public AiLogResult generateAiLog(GenerateDeadlineCommand command) { + validateCommand(command); + + String prompt = createDeadlinePrompt(command); + + AiLog aiLog = aiLogRepository.save( + new AiLog( + command.relatedShipmentId(), + command.shipmentManagerId(), + prompt, + command.requestType() + ) + ); + + try { + AiResponseInfo result = aiGenerator.generate(prompt); + + aiLog.markSuccess( + result.responseText(), + result.finalDeadlineAt() + ); + + return AiLogResult.from(aiLog); + + } catch (BusinessException e) { + aiLog.markFail(); + throw e; + } catch (Exception e) { + aiLog.markFail(); + throw new BusinessException(AiErrorCode.AI_GENERATE_FAILED); + } + } + + public AiLogResult getAiLog(UUID aiId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + + return AiLogResult.from(aiLog); + } + + public Page getAiLogs(Pageable pageable) { + return aiLogRepository.findAllByDeletedAtIsNull(pageable) + .map(AiLogResult::from); + } + + private void validateCommand(GenerateDeadlineCommand command) { + if (command == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_NOT_FOUND); + } + if (command.requestType() == null) { + throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); + } + if (command.requestType() != AiRequestType.DEADLINE) { + throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); + } + if (command.fromHub() == null || command.fromHub().isBlank()) { + throw new BusinessException(AiErrorCode.AI_FROM_HUB_REQUIRED); + } + if (command.toHub() == null || command.toHub().isBlank()) { + throw new BusinessException(AiErrorCode.AI_TO_HUB_REQUIRED); + } + if (command.product() == null || command.product().isBlank()) { + throw new BusinessException(AiErrorCode.AI_PRODUCT_REQUIRED); + } + if (command.deadline() == null) { + throw new BusinessException(AiErrorCode.AI_DEADLINE_REQUIRED); + } + } + + private String createDeadlinePrompt(GenerateDeadlineCommand command) { + String routeText = (command.route() == null || command.route().isEmpty()) + ? "없음" + : String.join(", ", command.route()); + + String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) + ? "없음" + : command.requestNote(); + + String workingHours = (command.workingHours() == null || command.workingHours().isBlank()) + ? DEFAULT_WORKING_HOURS + : command.workingHours(); + + return """ + 다음 물류 정보를 바탕으로 최종 발송 시한을 계산해라. + + 발송지: %s + 경유지: %s + 도착지: %s + 상품: %s + 요청사항: %s + 납기: %s + 근무시간: %s + + 반드시 ISO-8601 형식의 발송 시한만 포함해서 응답해라. + 예시: 2026-04-04T09:00:00 + """.formatted( + command.fromHub(), + routeText, + command.toHub(), + command.product(), + requestNote, + command.deadline(), + workingHours + ); + } + +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java similarity index 65% rename from notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java rename to notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java index cbfded5..2128838 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/vo/AiRequestInfo.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java @@ -1,13 +1,15 @@ -package com.shipflow.notificationservice.domain.ai.vo; +package com.shipflow.notificationservice.application.ai.dto.command; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; -public record AiRequestInfo( - //이벤트 +public record GenerateDeadlineCommand( + UUID relatedShipmentId, + UUID shipmentManagerId, String fromHub, String toHub, List route, @@ -16,7 +18,6 @@ public record AiRequestInfo( LocalDateTime deadline, String workingHours, AiRequestType requestType, - //도전기능 LocalDate workDate ) { } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/result/AiLogResult.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/result/AiLogResult.java new file mode 100644 index 0000000..b56d1d4 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/result/AiLogResult.java @@ -0,0 +1,38 @@ +package com.shipflow.notificationservice.application.ai.dto.result; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; + +public record AiLogResult( + UUID aiId, + UUID relatedShipmentId, + UUID shipmentManagerId, + String prompt, + String responseText, + LocalDateTime finalDeadlineAt, + LocalDate workDate, + SlackSendStatus sendStatus, + AiRequestType requestType, + AiRequestStatus requestStatus +) { + public static AiLogResult from(AiLog aiLog) { + return new AiLogResult( + aiLog.getId(), + aiLog.getRelatedShipmentId(), + aiLog.getShipmentManagerId(), + aiLog.getPrompt(), + aiLog.getResponseText(), + aiLog.getFinalDeadlineAt(), + aiLog.getWorkDate(), + aiLog.getSendStatus(), + aiLog.getRequestType(), + aiLog.getRequestStatus() + ); + } +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java index 6993595..86e2a68 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiGenerator.java @@ -1,9 +1,7 @@ package com.shipflow.notificationservice.domain.ai; -import com.shipflow.notificationservice.domain.ai.vo.AiRequestInfo; import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; public interface AiGenerator { - - AiResponseInfo generate(AiRequestInfo requestInfo); -} + AiResponseInfo generate(String prompt); +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java index 4c0f573..92ceb34 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java @@ -15,6 +15,7 @@ public enum AiErrorCode implements ErrorCode { AI_REQUEST_TYPE_REQUIRED("AI_REQUEST_TYPE_REQUIRED", HttpStatus.BAD_REQUEST, "AI 요청 타입은 필수입니다."), // 필수 데이터 + AI_PROMPT_REQUIRED("AI_PROMPT_REQUIRED", HttpStatus.BAD_REQUEST, "AI 프롬프트는 필수입니다."), AI_FROM_HUB_REQUIRED("AI_FROM_HUB_REQUIRED", HttpStatus.BAD_REQUEST, "출발 허브 정보는 필수입니다."), AI_TO_HUB_REQUIRED("AI_TO_HUB_REQUIRED", HttpStatus.BAD_REQUEST, "도착 허브 정보는 필수입니다."), AI_PRODUCT_REQUIRED("AI_PRODUCT_REQUIRED", HttpStatus.BAD_REQUEST, "상품 정보는 필수입니다."), diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java index 340e967..1f92ea7 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java @@ -12,7 +12,6 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.domain.ai.AiGenerator; import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; -import com.shipflow.notificationservice.domain.ai.vo.AiRequestInfo; import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; import com.shipflow.notificationservice.infrastructure.client.ai.config.GeminiProperties; import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiRequest; @@ -21,24 +20,23 @@ import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; -@RequiredArgsConstructor @Component +@RequiredArgsConstructor public class GeminiApiClient implements AiGenerator { private static final String API_KEY_HEADER = "x-goog-api-key"; private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); private static final Pattern DEADLINE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2})?"); - private static final String WORKING_HOURS = "09:00 ~ 18:00"; private final WebClient webClient; private final GeminiProperties geminiProperties; @Override - public AiResponseInfo generate(AiRequestInfo aiRequestInfo) { - validateRequestInfo(aiRequestInfo); + public AiResponseInfo generate(String prompt) { + validatePrompt(prompt); + GeminiResponse response; - String prompt = createPrompt(aiRequestInfo); try { response = webClient.post() @@ -46,19 +44,14 @@ public AiResponseInfo generate(AiRequestInfo aiRequestInfo) { .header(API_KEY_HEADER, geminiProperties.getApiKey()) .bodyValue(new GeminiRequest(prompt)) .retrieve() - - // 4xx 에러 처리 (잘못된 요청) .onStatus( status -> status.is4xxClientError(), res -> Mono.error(new BusinessException(AiErrorCode.AI_GENERATE_FAILED)) ) - - // 5xx 에러 처리 (서버 오류) .onStatus( status -> status.is5xxServerError(), res -> Mono.error(new BusinessException(AiErrorCode.AI_GENERATE_FAILED)) ) - .bodyToMono(GeminiResponse.class) .timeout(REQUEST_TIMEOUT) .block(); @@ -74,63 +67,10 @@ public AiResponseInfo generate(AiRequestInfo aiRequestInfo) { return new AiResponseInfo(text, finalDeadlineAt); } - //프롬포트 - private String createPrompt(AiRequestInfo info) { - String routeText = (info.route() == null || info.route().isEmpty()) - ? "없음" - : String.join(", ", info.route()); - String requestNote = (info.requestNote() == null || info.requestNote().isBlank()) - ? "없음" - : info.requestNote(); - - return """ - 다음 물류 정보를 바탕으로 최종 발송 시한을 계산해라. - - 발송지: %s - 경유지: %s - 도착지: %s - 상품: %s - 요청사항: %s - 납기: %s - 근무시간: %s - - 반드시 ISO-8601 형식의 발송 시한만 포함해서 응답해라. - 예시: 2026-04-04T09:00:00 - """.formatted( - info.fromHub(), - routeText, - info.toHub(), - info.product(), - info.requestNote(), - info.deadline(), - WORKING_HOURS - ); - } - - //유효성 검증 - private void validateRequestInfo(AiRequestInfo aiRequestInfo) { - if (aiRequestInfo == null) { - throw new BusinessException(AiErrorCode.AI_EVENT_NOT_FOUND); - } - if (aiRequestInfo.requestType() == null) { - throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); - } - if (aiRequestInfo.fromHub() == null || aiRequestInfo.fromHub().isBlank()) { - throw new BusinessException(AiErrorCode.AI_FROM_HUB_REQUIRED); - } - - if (aiRequestInfo.toHub() == null || aiRequestInfo.toHub().isBlank()) { - throw new BusinessException(AiErrorCode.AI_TO_HUB_REQUIRED); - } - - if (aiRequestInfo.product() == null || aiRequestInfo.product().isBlank()) { - throw new BusinessException(AiErrorCode.AI_PRODUCT_REQUIRED); + private void validatePrompt(String prompt) { + if (prompt == null || prompt.isBlank()) { + throw new BusinessException(AiErrorCode.AI_PROMPT_REQUIRED); } - - if (aiRequestInfo.deadline() == null) { - throw new BusinessException(AiErrorCode.AI_DEADLINE_REQUIRED); - } - } private String extractText(GeminiResponse response) { @@ -166,5 +106,4 @@ private LocalDateTime extractDeadline(String text) { throw new BusinessException(AiErrorCode.AI_RESPONSE_PARSE_FAILED); } } - -}//끝 \ No newline at end of file +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java new file mode 100644 index 0000000..0a843af --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -0,0 +1,61 @@ +package com.shipflow.notificationservice.presentation.ai.dto.request; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record GenerateDeadlineRequest( + + @NotNull(message = "relatedShipmentId는 필수입니다.") + UUID relatedShipmentId, + + @NotNull(message = "shipmentManagerId는 필수입니다.") + UUID shipmentManagerId, + + @NotBlank(message = "fromHub는 필수입니다.") + String fromHub, + + @NotBlank(message = "toHub는 필수입니다.") + String toHub, + + List route, + + @NotBlank(message = "product는 필수입니다.") + String product, + + String requestNote, + + @NotNull(message = "deadline은 필수입니다.") + LocalDateTime deadline, + + String workingHours, + + @NotNull(message = "requestType은 필수입니다.") + AiRequestType requestType, + + LocalDate workDate +) { + + public GenerateDeadlineCommand toCommand() { + return new GenerateDeadlineCommand( + relatedShipmentId, + shipmentManagerId, + fromHub, + toHub, + route, + product, + requestNote, + deadline, + workingHours, + requestType, + workDate + ); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/response/AiLogResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/response/AiLogResponse.java new file mode 100644 index 0000000..5248eb8 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/response/AiLogResponse.java @@ -0,0 +1,39 @@ +package com.shipflow.notificationservice.presentation.ai.dto.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; + +public record AiLogResponse( + UUID aiId, + UUID relatedShipmentId, + UUID shipmentManagerId, + String prompt, + String responseText, + LocalDateTime finalDeadlineAt, + LocalDate workDate, + SlackSendStatus sendStatus, + AiRequestType requestType, + AiRequestStatus requestStatus +) { + + public static AiLogResponse from(AiLogResult result) { + return new AiLogResponse( + result.aiId(), + result.relatedShipmentId(), + result.shipmentManagerId(), + result.prompt(), + result.responseText(), + result.finalDeadlineAt(), + result.workDate(), + result.sendStatus(), + result.requestType(), + result.requestStatus() + ); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java new file mode 100644 index 0000000..7683f4a --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -0,0 +1,58 @@ +package com.shipflow.notificationservice.presentation.ai.external; + +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; +import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; + +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/ai") +public class AiController { + + private final AiAppService aiAppService; + + public AiController(AiAppService aiAppService) { + this.aiAppService = aiAppService; + } + + @PostMapping + public ApiResponse generateAiLog( + @Valid @RequestBody GenerateDeadlineRequest request + ) { + return ApiResponse.ok( + AiLogResponse.from( + aiAppService.generateAiLog(request.toCommand()) + ) + ); + } + + @GetMapping("/{aiId}") + public ApiResponse getAiLog(@PathVariable UUID aiId) { + return ApiResponse.ok( + AiLogResponse.from( + aiAppService.getAiLog(aiId) + ) + ); + } + + @GetMapping + public ApiResponse> getAiLogs(Pageable pageable) { + return ApiResponse.ok( + aiAppService.getAiLogs(pageable) + .map(AiLogResponse::from) + ); + } +} \ No newline at end of file From 8ca44705f037f16234405e8cad2bae3c6d9e6330 Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 02:05:28 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat(AI):=20RabbitMQ=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=88=98=EC=8B=A0=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=B4=88=EA=B8=B0=20=EC=84=A4=EC=A0=95(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notification-service/build.gradle | 1 + .../client/ai/GeminiApiClient.java | 3 +- .../client/ai/dto/GeminiRequest.java | 1 + .../config/NotificationRabbitConfig.java | 46 +++++++++++++++++++ .../consumer/ShipmentCreatedConsumer.java | 32 +++++++++++++ .../messaging/dto/ShipmentCreatedEvent.java | 24 ++++++++++ .../src/main/resources/application.yaml | 10 +++- 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/config/NotificationRabbitConfig.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java diff --git a/notification-service/build.gradle b/notification-service/build.gradle index f3b776e..7f6e30f 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -47,6 +47,7 @@ dependencies { // 6. Common Module implementation project(':common') + implementation 'org.springframework.boot:spring-boot-starter-amqp' // 7. Lombok compileOnly 'org.projectlombok:lombok' diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java index 1f92ea7..e06fa99 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java @@ -34,14 +34,15 @@ public class GeminiApiClient implements AiGenerator { @Override public AiResponseInfo generate(String prompt) { + validatePrompt(prompt); - GeminiResponse response; try { response = webClient.post() .uri(geminiProperties.getUrl()) .header(API_KEY_HEADER, geminiProperties.getApiKey()) + .header("Content-Type", "application/json") .bodyValue(new GeminiRequest(prompt)) .retrieve() .onStatus( diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java index 58cd0b5..f27223a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/dto/GeminiRequest.java @@ -4,6 +4,7 @@ import lombok.Getter; +@Getter public class GeminiRequest { private List contents; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/config/NotificationRabbitConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/config/NotificationRabbitConfig.java new file mode 100644 index 0000000..8fdc246 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/config/NotificationRabbitConfig.java @@ -0,0 +1,46 @@ +package com.shipflow.notificationservice.infrastructure.messaging.config; + +import static org.springframework.amqp.core.BindingBuilder.*; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.shipflow.config.message.RabbitMqConfig; + +@Configuration +public class NotificationRabbitConfig { + + private static final String ROUTING_SHIPMENT_CREATED = "shipment.created"; + + public static final String QUEUE_NOTIFICATION_SHIPMENT_CREATED = "notification.shipment.created"; + public static final String QUEUE_NOTIFICATION_SHIPMENT_CREATED_DLQ = + QUEUE_NOTIFICATION_SHIPMENT_CREATED + ".dlq"; + + @Bean + public Queue queueNotificationShipmentCreated() { + return RabbitMqConfig.durableQueue(QUEUE_NOTIFICATION_SHIPMENT_CREATED); + } + + @Bean + public Queue queueNotificationShipmentCreatedDlq() { + return RabbitMqConfig.dlqQueue(QUEUE_NOTIFICATION_SHIPMENT_CREATED_DLQ); + } + + @Bean + public Binding bindNotificationShipmentCreated(TopicExchange sagaExchange) { + return bind(queueNotificationShipmentCreated()) + .to(sagaExchange) + .with(ROUTING_SHIPMENT_CREATED); + } + + @Bean + public Binding bindNotificationShipmentCreatedDlq(DirectExchange sagaDlx) { + return bind(queueNotificationShipmentCreatedDlq()) + .to(sagaDlx) + .with(QUEUE_NOTIFICATION_SHIPMENT_CREATED_DLQ); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java new file mode 100644 index 0000000..6b7468e --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java @@ -0,0 +1,32 @@ +package com.shipflow.notificationservice.infrastructure.messaging.consumer; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class ShipmentCreatedConsumer { + + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) + public void handleShipmentCreated(ShipmentCreatedEvent event) { + log.info( + "[ShipmentCreatedConsumer] shipment.created 수신 - orderId={}, ordererId={}, supplierCompanyId={}, receiverCompanyId={}, productId={}, quantity={}, departureHubId={}, arrivalHubId={}, requestDeadline={}, requestNote={}, occurredAt={}", + event.getOrderId(), + event.getOrdererId(), + event.getSupplierCompanyId(), + event.getReceiverCompanyId(), + event.getProductId(), + event.getQuantity(), + event.getDepartureHubId(), + event.getArrivalHubId(), + event.getRequestDeadline(), + event.getRequestNote(), + event.getOccurredAt() + ); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java new file mode 100644 index 0000000..0f056a0 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -0,0 +1,24 @@ +package com.shipflow.notificationservice.infrastructure.messaging.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ShipmentCreatedEvent { + + private UUID orderId; + private UUID ordererId; + private UUID supplierCompanyId; + private UUID receiverCompanyId; + private UUID productId; + private Integer quantity; + private UUID departureHubId; + private UUID arrivalHubId; + private LocalDateTime requestDeadline; + private String requestNote; + private LocalDateTime occurredAt; +} \ No newline at end of file diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index 826ed52..b09de44 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -9,6 +9,8 @@ spring: password: ${DB_PASSWORD} jpa: + hibernate: + ddl-auto: update properties: hibernate: default_schema: notification @@ -16,9 +18,15 @@ spring: config: import: optional:file:.env + rabbitmq: + host: ${RABBITMQ_HOST:localhost} + port: ${RABBITMQ_PORT:5672} + username: ${RABBITMQ_USERNAME:guest} + password: ${RABBITMQ_PASSWORD:guest} + slack: bot-token: ${SLACK_BOT_TOKEN} gemini: api-key: ${GEMINI_API_KEY} - url: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent \ No newline at end of file + url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent \ No newline at end of file From 564e1623ba49802d4b0e3bda2ce445eab283eb77 Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 14:06:56 +0900 Subject: [PATCH 06/20] =?UTF-8?q?test(AI):=20AiAppService=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationservice/AiAppServiceTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java new file mode 100644 index 0000000..c69fb50 --- /dev/null +++ b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java @@ -0,0 +1,187 @@ +package com.shipflow.notificationservice; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.domain.ai.AiGenerator; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; + +@ExtendWith(MockitoExtension.class) +class AiAppServiceTest { + + @Mock + private AiLogRepository aiLogRepository; + + @Mock + private AiGenerator aiGenerator; + + @InjectMocks + private AiAppService aiAppService; + + @Test + @DisplayName("AI 생성 성공") + void generate_success() { + GenerateDeadlineCommand command = validCommand(); + + when(aiLogRepository.save(any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + + AiResponseInfo response = mock(AiResponseInfo.class); + when(response.responseText()).thenReturn("2026-04-10T09:00:00"); + when(response.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + when(aiGenerator.generate(anyString())) + .thenReturn(response); + + AiLogResult result = aiAppService.generateAiLog(command); + + assertThat(result.requestStatus()).isEqualTo(AiRequestStatus.SUCCESS); + assertThat(result.responseText()).isEqualTo("2026-04-10T09:00:00"); + } + + @Test + @DisplayName("AI 생성 실패") + void generate_fail_business() { + GenerateDeadlineCommand command = validCommand(); + + when(aiLogRepository.save(any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + + when(aiGenerator.generate(anyString())) + .thenThrow(new BusinessException(AiErrorCode.AI_GENERATE_FAILED)); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + + //유효성 검증 + @Nested + class ValidateTest { + + @Test + void null_command() { + assertThatThrownBy(() -> aiAppService.generateAiLog(null)) + .isInstanceOf(BusinessException.class); + } + + @Test + void requestType_null() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + when(command.requestType()).thenReturn(null); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + + @Test + void fromHub_blank() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + + when(command.requestType()).thenReturn(AiRequestType.DEADLINE); + when(command.fromHub()).thenReturn(" "); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + + @Test + void toHub_blank() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + + when(command.requestType()).thenReturn(AiRequestType.DEADLINE); + when(command.fromHub()).thenReturn("정상"); + when(command.toHub()).thenReturn(" "); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + + @Test + void product_blank() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + + when(command.requestType()).thenReturn(AiRequestType.DEADLINE); + when(command.fromHub()).thenReturn("정상"); + when(command.toHub()).thenReturn("정상"); + when(command.product()).thenReturn(" "); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + + @Test + void deadline_null() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + + when(command.requestType()).thenReturn(AiRequestType.DEADLINE); + when(command.fromHub()).thenReturn("정상"); + when(command.toHub()).thenReturn("정상"); + when(command.product()).thenReturn("상품"); + when(command.deadline()).thenReturn(null); + + assertThatThrownBy(() -> aiAppService.generateAiLog(command)) + .isInstanceOf(BusinessException.class); + } + } + + // ========================= + // 조회 + // ========================= + + @Test + void get_success() { + UUID id = UUID.randomUUID(); + + AiLog aiLog = new AiLog( + UUID.randomUUID(), + UUID.randomUUID(), + "prompt", + AiRequestType.DEADLINE + ); + + aiLog.markSuccess("ok", LocalDateTime.now()); + + when(aiLogRepository.findByIdAndDeletedAtIsNull(id)) + .thenReturn(Optional.of(aiLog)); + + AiLogResult result = aiAppService.getAiLog(id); + + assertThat(result.requestStatus()).isEqualTo(AiRequestStatus.SUCCESS); + } + + private GenerateDeadlineCommand validCommand() { + GenerateDeadlineCommand command = mock(GenerateDeadlineCommand.class); + + when(command.relatedShipmentId()).thenReturn(UUID.randomUUID()); + when(command.shipmentManagerId()).thenReturn(UUID.randomUUID()); + when(command.requestType()).thenReturn(AiRequestType.DEADLINE); + when(command.fromHub()).thenReturn("경기"); + when(command.toHub()).thenReturn("부산"); + when(command.product()).thenReturn("상품"); + when(command.deadline()).thenReturn(LocalDateTime.now()); + + return command; + } +} \ No newline at end of file From 7d6e6512fbad59961b7f8217dc32fc6d62d1bd3f Mon Sep 17 00:00:00 2001 From: 250ghghghgh Date: Sun, 5 Apr 2026 14:31:26 +0900 Subject: [PATCH 07/20] Update notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../infrastructure/messaging/dto/ShipmentCreatedEvent.java | 1 + 1 file changed, 1 insertion(+) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 0f056a0..6c529b6 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -10,6 +10,7 @@ @NoArgsConstructor public class ShipmentCreatedEvent { + private UUID shipmentId; private UUID orderId; private UUID ordererId; private UUID supplierCompanyId; From e175f84cea12cf057ef0e0decaa406d022208de7 Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 15:10:31 +0900 Subject: [PATCH 08/20] =?UTF-8?q?refactor(AI):=20Copilot=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ai/AiAppService.java | 3 +-- .../presentation/ai/external/AiController.java | 10 ++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index 0d7fd9b..882e990 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -83,7 +83,7 @@ private void validateCommand(GenerateDeadlineCommand command) { throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); } if (command.requestType() != AiRequestType.DEADLINE) { - throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); } if (command.fromHub() == null || command.fromHub().isBlank()) { throw new BusinessException(AiErrorCode.AI_FROM_HUB_REQUIRED); @@ -135,5 +135,4 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { workingHours ); } - } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index 7683f4a..5a942ef 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -2,7 +2,6 @@ import java.util.UUID; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -15,6 +14,7 @@ import com.shipflow.notificationservice.application.ai.AiAppService; import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; +import com.shipflow.notificationservice.presentation.common.BasePageResponse; import jakarta.validation.Valid; @@ -49,10 +49,12 @@ public ApiResponse getAiLog(@PathVariable UUID aiId) { } @GetMapping - public ApiResponse> getAiLogs(Pageable pageable) { + public ApiResponse> getAiLogs(Pageable pageable) { return ApiResponse.ok( - aiAppService.getAiLogs(pageable) - .map(AiLogResponse::from) + BasePageResponse.from( + aiAppService.getAiLogs(pageable) + .map(AiLogResponse::from) + ) ); } } \ No newline at end of file From 92bb3faea12fc9b4c8b4da4fdd8d5bbcca861bb7 Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 16:08:08 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat(notification):=20Slack=20=EB=B0=8F?= =?UTF-8?q?=20AI=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=A0=81=EC=9A=A9(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/slack/SlackAppService.java | 15 +++++++----- .../repository/SlackMessageRepository.java | 6 +++-- .../slack/SlackMessageJpaRepository.java | 8 +++---- .../slack/SlackMessageRepositoryImpl.java | 7 +++--- .../dto/request/SendSlackMessageRequest.java | 13 +++++++++++ .../request/UpdateSlackMessageRequest.java | 5 ++++ .../dto/response/SlackMessageResponse.java | 6 ----- .../slack/external/SlackController.java | 23 ++++++++++++++----- 8 files changed, 56 insertions(+), 27 deletions(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 8abf7ce..6e49c8c 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -1,8 +1,9 @@ package com.shipflow.notificationservice.application.slack; -import java.util.List; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +16,7 @@ import com.shipflow.notificationservice.domain.slack.exception.SlackErrorCode; import com.shipflow.notificationservice.domain.slack.repository.SlackMessageRepository; import com.shipflow.notificationservice.domain.slack.vo.SlackSendInfo; +import com.shipflow.notificationservice.presentation.common.BasePageRequest; import lombok.RequiredArgsConstructor; @@ -63,11 +65,12 @@ public SlackMessageResult getSlackMessage(UUID slackId) { } // 목록 조회 - public List getSlackMessages() { - return slackMessageRepository.findAllByDeletedAtIsNull() - .stream() - .map(SlackMessageResult::from) - .toList(); + public Page getSlackMessages(BasePageRequest pageRequest) { + + Pageable pageable = pageRequest.toPageable(); + + return slackMessageRepository.findAllByDeletedAtIsNull(pageable) + .map(SlackMessageResult::from); } //슬랙 메세지 수정 diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java index ebe19f2..0e7a638 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java @@ -1,9 +1,11 @@ package com.shipflow.notificationservice.domain.slack.repository; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + import com.shipflow.notificationservice.domain.slack.SlackMessage; public interface SlackMessageRepository { @@ -12,5 +14,5 @@ public interface SlackMessageRepository { Optional findByIdAndDeletedAtIsNull(UUID slackId); - List findAllByDeletedAtIsNull(); + Page findAllByDeletedAtIsNull(Pageable pageable); } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java index 9d1a8b1..d1cc5b8 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java @@ -1,9 +1,10 @@ package com.shipflow.notificationservice.infrastructure.persistence.slack; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.hibernate.query.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.shipflow.notificationservice.domain.slack.SlackMessage; @@ -14,6 +15,5 @@ public interface SlackMessageJpaRepository extends JpaRepository findByIdAndDeletedAtIsNull(UUID id); // TODO: 목록 조회 페이징 및 검색 처리 필요 - List findAllByDeletedAtIsNull(); - -} \ No newline at end of file + Page findAllByDeletedAtIsNull(Pageable pageable); +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java index 14a7307..6fb28ad 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java @@ -1,9 +1,10 @@ package com.shipflow.notificationservice.infrastructure.persistence.slack; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.hibernate.query.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import com.shipflow.notificationservice.domain.slack.SlackMessage; @@ -28,7 +29,7 @@ public Optional findByIdAndDeletedAtIsNull(UUID slackId) { } @Override - public List findAllByDeletedAtIsNull() { - return slackMessageJpaRepository.findAllByDeletedAtIsNull(); + public Page findAllByDeletedAtIsNull(Pageable pageable) { + return slackMessageJpaRepository.findAllByDeletedAtIsNull(pageable); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java index 5b6b19a..85a5173 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java @@ -5,11 +5,24 @@ import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + public record SendSlackMessageRequest( + @NotBlank + @Pattern(regexp = "^[UC][A-Z0-9]+$", message = "올바른 Slack ID 형식이 아닙니다.") String receiverSlackId, + UUID relatedShipmentId, UUID relatedAiLogId, + + @NotBlank + @Size(max = 1000) String message, + + @NotNull SlackMessageType messageType ) { public SendSlackMessageCommand toCommand() { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java index 1a7fd2b..c6c2a2f 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java @@ -4,7 +4,12 @@ import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + public record UpdateSlackMessageRequest( + @NotBlank + @Size(max = 1000) String message ) { public UpdateSlackMessageCommand toCommand(UUID slackId) { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java index ad7bc30..487ff42 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java @@ -1,7 +1,6 @@ package com.shipflow.notificationservice.presentation.slack.dto.response; import java.time.LocalDateTime; -import java.util.List; import java.util.UUID; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -35,9 +34,4 @@ public static SlackMessageResponse from(SlackMessageResult result) { ); } - public static List from(List results) { - return results.stream() - .map(SlackMessageResponse::from) - .toList(); - } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java index 869d89c..d6315ec 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java @@ -1,10 +1,10 @@ package com.shipflow.notificationservice.presentation.slack.external; -import java.util.List; import java.util.UUID; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -14,10 +14,14 @@ import com.shipflow.common.exception.ApiResponse; import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.presentation.common.BasePageRequest; +import com.shipflow.notificationservice.presentation.common.BasePageResponse; import com.shipflow.notificationservice.presentation.slack.dto.request.SendSlackMessageRequest; import com.shipflow.notificationservice.presentation.slack.dto.request.UpdateSlackMessageRequest; import com.shipflow.notificationservice.presentation.slack.dto.response.SlackMessageResponse; +import jakarta.validation.Valid; + @RestController @RequestMapping("/api/slack") public class SlackController { @@ -31,7 +35,7 @@ public SlackController(SlackAppService slackAppService) { } @PostMapping - public ApiResponse sendSlackMessage(@RequestBody SendSlackMessageRequest request) { + public ApiResponse sendSlackMessage(@Valid @RequestBody SendSlackMessageRequest request) { return ApiResponse.ok( SlackMessageResponse.from( slackAppService.sendSlackMessage(request.toCommand()) @@ -44,16 +48,23 @@ public ApiResponse getSlackMessage(@PathVariable UUID slac return ApiResponse.ok(SlackMessageResponse.from(slackAppService.getSlackMessage(slackId))); } - // TODO: 목록 조회 페이징 및 검색 처리 필요 + // TODO: 목록 조회 검색 처리 필요 @GetMapping - public ApiResponse> getAllSlackMessages() { - return ApiResponse.ok(SlackMessageResponse.from(slackAppService.getSlackMessages())); + public ApiResponse> getAllSlackMessages( + @ModelAttribute BasePageRequest pageRequest + ) { + return ApiResponse.ok( + BasePageResponse.from( + slackAppService.getSlackMessages(pageRequest) + .map(SlackMessageResponse::from) + ) + ); } @PatchMapping("/{slackId}") public ApiResponse updateSlackMessage( @PathVariable UUID slackId, - @RequestBody UpdateSlackMessageRequest request + @Valid @RequestBody UpdateSlackMessageRequest request ) { return ApiResponse.ok( SlackMessageResponse.from( From bee750060abbd6ff3f37be685d4e746a9835d146 Mon Sep 17 00:00:00 2001 From: 250 Date: Sun, 5 Apr 2026 23:35:34 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat(Notification):=20AI-Slack=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ai/AiAppService.java | 85 +++++++++++++++++++ .../messaging/dto/ShipmentCreatedEvent.java | 3 + .../slack/SlackMessageJpaRepository.java | 2 +- .../slack/SlackMessageRepositoryImpl.java | 2 +- .../dto/request/GenerateDeadlineRequest.java | 3 + .../ai/external/AiController.java | 16 ++++ .../presentation/common/BasePageRequest.java | 8 ++ 7 files changed, 117 insertions(+), 2 deletions(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index 882e990..bd4fe9a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -10,12 +10,15 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.domain.ai.AiGenerator; import com.shipflow.notificationservice.domain.ai.AiLog; import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; import lombok.RequiredArgsConstructor; @@ -28,7 +31,9 @@ public class AiAppService { private final AiLogRepository aiLogRepository; private final AiGenerator aiGenerator; + private final SlackAppService slackAppService; + //테스트용 외부(AI만 실행) @Transactional public AiLogResult generateAiLog(GenerateDeadlineCommand command) { validateCommand(command); @@ -63,6 +68,35 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { } } + //슬랙으로 전송 + //AI 실행 → 결과로 Slack 발송 → sendStatus 업데이트 + @Transactional + public AiLogResult generateAndSendSlack(GenerateDeadlineCommand command, String receiverSlackId) { + + AiLogResult aiResult = generateAiLog(command); + + String slackMessage = createDeadlineSlackMessage(command, aiResult); + + try { + slackAppService.sendSlackMessage( + new SendSlackMessageCommand( + receiverSlackId, + command.relatedShipmentId(), + aiResult.aiId(), + slackMessage, + SlackMessageType.DEADLINE_ALERT + ) + ); + + markSlackSendSuccess(aiResult.aiId()); + return aiResult; + + } catch (Exception e) { + markSlackSendFail(aiResult.aiId()); + throw e; + } + } + public AiLogResult getAiLog(UUID aiId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); @@ -99,6 +133,7 @@ private void validateCommand(GenerateDeadlineCommand command) { } } + // AI 요청용 프롬프트 (AI 입력) private String createDeadlinePrompt(GenerateDeadlineCommand command) { String routeText = (command.route() == null || command.route().isEmpty()) ? "없음" @@ -135,4 +170,54 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { workingHours ); } + + // Slack 메시지용 (사용자에게 보여줄 출력) + private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLogResult aiResult) { + + String routeText = (command.route() == null || command.route().isEmpty()) + ? "없음" + : String.join(" → ", command.route()); + + String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) + ? "없음" + : command.requestNote(); + + return """ + 🚚 배송 요청 알림 + + 상품 정보: %s + 요청 사항: %s + + 발송지: %s + 경유지: %s + 도착지: %s + + ⏰ AI 계산 최종 발송 시한: %s + + ※ 해당 시간 이전에 발송을 완료해주세요. + """.formatted( + command.product(), + requestNote, + command.fromHub(), + routeText, + command.toHub(), + aiResult.finalDeadlineAt() + ); + } + + // Slack 성공 시 AiLog 상태 업데이트 + private void markSlackSendSuccess(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + + aiLog.markSendSuccess(); + } + + // Slack 실패 시 AiLog 상태 업데이트 + private void markSlackSendFail(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + + aiLog.markSendFail(); + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 6c529b6..cfe940d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -3,6 +3,8 @@ import java.time.LocalDateTime; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,6 +12,7 @@ @NoArgsConstructor public class ShipmentCreatedEvent { + @JsonIgnoreProperties(ignoreUnknown = true) private UUID shipmentId; private UUID orderId; private UUID ordererId; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java index d1cc5b8..ae56d4d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java @@ -3,7 +3,7 @@ import java.util.Optional; import java.util.UUID; -import org.hibernate.query.Page; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java index 6fb28ad..4639c1d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java @@ -3,7 +3,7 @@ import java.util.Optional; import java.util.UUID; -import org.hibernate.query.Page; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java index 0a843af..708203e 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -40,6 +40,9 @@ public record GenerateDeadlineRequest( @NotNull(message = "requestType은 필수입니다.") AiRequestType requestType, + @NotBlank(message = "receiverSlackId는 필수입니다.") + String receiverSlackId, + LocalDate workDate ) { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index 5a942ef..c58d33f 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -12,6 +12,7 @@ import com.shipflow.common.exception.ApiResponse; import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; import com.shipflow.notificationservice.presentation.common.BasePageResponse; @@ -28,6 +29,7 @@ public AiController(AiAppService aiAppService) { this.aiAppService = aiAppService; } + // TODO: 이벤트 기반 전환 후 admin/debug 용으로 유지 @PostMapping public ApiResponse generateAiLog( @Valid @RequestBody GenerateDeadlineRequest request @@ -39,6 +41,20 @@ public ApiResponse generateAiLog( ); } + // TODO: 이벤트 기반 처리 완료 후 삭제 예정 (AI → Slack 테스트용) + // 임시 컨트롤러 + @PostMapping("/test-slack") + public ApiResponse testSlack( + @Valid @RequestBody GenerateDeadlineRequest request + ) { + AiLogResult result = aiAppService.generateAndSendSlack( + request.toCommand(), + request.receiverSlackId() + ); + + return ApiResponse.ok(AiLogResponse.from(result)); + } + @GetMapping("/{aiId}") public ApiResponse getAiLog(@PathVariable UUID aiId) { return ApiResponse.ok( diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java index c735b4b..271e945 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java @@ -16,6 +16,14 @@ public record BasePageRequest( } } + public Pageable toPageable() { + return PageRequest.of( + page, + size, + Sort.by(Sort.Direction.DESC, "createdAt") + ); + } + public Pageable toPageable(Sort sort) { return PageRequest.of(page, size, sort); } From 0c036f0479d3b5e85cdedb3f2052620adeb08ba0 Mon Sep 17 00:00:00 2001 From: 250 Date: Mon, 6 Apr 2026 01:09:13 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat(notification):=20shipment.created=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B8=B0=EB=B0=98=20AI=20?= =?UTF-8?q?=E2=86=92=20Slack=20=EC=9E=90=EB=8F=99=20=EB=B0=9C=EC=86=A1=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EA=B5=AC=ED=98=84(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keycloak/shipflow-export.json | 4093 ++++++++++------- .../NotificationOrchestratorService.java | 148 + .../application/ai/AiAppService.java | 83 - .../consumer/ShipmentCreatedConsumer.java | 21 +- .../messaging/dto/ShipmentCreatedEvent.java | 3 +- .../ai/external/AiController.java | 15 - 6 files changed, 2493 insertions(+), 1870 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java diff --git a/keycloak/shipflow-export.json b/keycloak/shipflow-export.json index 0f59194..ad9925d 100644 --- a/keycloak/shipflow-export.json +++ b/keycloak/shipflow-export.json @@ -1,1799 +1,2372 @@ { - "id" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "realm" : "shipflow", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 300, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : false, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxTemporaryLockouts" : 0, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "9c6ebe92-6809-44ff-b785-61ca2ec674a5", - "name" : "COMPANY_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "437b33b6-c49b-4cd6-9e1e-de118ba010ff", - "name" : "HUB_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "dbea48c0-3ebf-49e9-acb1-60036fc05185", - "name" : "MASTER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name" : "default-roles-shipflow", - "description" : "${role_default-roles}", - "composite" : false, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ], - "client" : { - "account" : [ "manage-account", "view-profile" ] - } + "id": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "realm": "shipflow", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": false, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "9c6ebe92-6809-44ff-b785-61ca2ec674a5", + "name": "COMPANY_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "437b33b6-c49b-4cd6-9e1e-de118ba010ff", + "name": "HUB_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} }, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "d17dde63-adeb-41df-838d-c8ee55622c70", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "febb44ff-b3c0-4870-a9e8-d84afb05bc51", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", - "name" : "SHIPMENT_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - } ], - "client" : { - "realm-management" : [ { - "id" : "5ac95120-9c91-42fc-a9df-6e38970dad79", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "caa610f3-4986-42d7-b720-6bd8132da76a", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "9e5a736d-34b7-4aac-b8ac-7cd14650f379", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "812427c2-0481-4ba6-9e67-ee98739d7489", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-clients" ] + { + "id": "dbea48c0-3ebf-49e9-acb1-60036fc05185", + "name": "MASTER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": false, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] } }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "8c3c30fe-95a8-413e-85b0-0f963e5a643c", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "0d7742bd-b6dc-4ca6-ab61-3f516d540624", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "cab6e153-818c-467f-9bb2-652120c2fe4d", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ebb9d35e-deb8-4b59-86df-0cef09d4640c", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "5237356a-5e32-4ddb-bac7-4e521a56326e", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ce72a656-73d8-49c9-a04e-cfefd0e89d13", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-users", "query-groups" ] - } + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d17dde63-adeb-41df-838d-c8ee55622c70", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "febb44ff-b3c0-4870-a9e8-d84afb05bc51", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", + "name": "SHIPMENT_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "5ac95120-9c91-42fc-a9df-6e38970dad79", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "c5fa6724-d145-484d-8f33-dafa65b13b74", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "d5324b44-3656-4978-bb13-96915cdf60ff", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ef01e291-c2c5-4d47-bc83-e50a1971ee55", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "34d178dc-ed4b-4517-b261-de55f93c7ab9", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "de1f054b-15ad-4839-a54e-537039a31820", - "name" : "realm-admin", - "description" : "${role_realm-admin}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "manage-realm", "view-clients", "manage-clients", "query-clients", "view-authorization", "view-realm", "manage-events", "create-client", "query-realms", "view-events", "view-identity-providers", "view-users", "query-users", "manage-authorization", "manage-identity-providers", "impersonation", "manage-users", "query-groups" ] - } + { + "id": "caa610f3-4986-42d7-b720-6bd8132da76a", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "5df84e42-5988-4b59-a8eb-b83835f1f662", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "account-console" : [ ], - "broker" : [ { - "id" : "d66bcb04-700d-483c-9628-207d0bd431bd", - "name" : "read-token", - "description" : "${role_read-token}", - "composite" : false, - "clientRole" : true, - "containerId" : "0b06878c-6957-40d0-8783-23e075fcf103", - "attributes" : { } - } ], - "account" : [ { - "id" : "044b18e6-b560-43c8-925d-80fbce6ddc28", - "name" : "manage-consent", - "description" : "${role_manage-consent}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "view-consent" ] - } + { + "id": "9e5a736d-34b7-4aac-b8ac-7cd14650f379", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} }, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "fa040d5d-ca2f-4386-85bd-75716818b4a0", - "name" : "manage-account-links", - "description" : "${role_manage-account-links}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "76c4a106-bd5d-43b2-9098-708ea5813f2a", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "c99f7465-dd93-4f8d-ab26-64439d75f0c2", - "name" : "view-applications", - "description" : "${role_view-applications}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "43c296b2-6d12-41f8-898a-2663dde41447", - "name" : "view-consent", - "description" : "${role_view-consent}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", - "name" : "manage-account", - "description" : "${role_manage-account}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "manage-account-links" ] - } + { + "id": "812427c2-0481-4ba6-9e67-ee98739d7489", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "8c3c30fe-95a8-413e-85b0-0f963e5a643c", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d7742bd-b6dc-4ca6-ab61-3f516d540624", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "cab6e153-818c-467f-9bb2-652120c2fe4d", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ebb9d35e-deb8-4b59-86df-0cef09d4640c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5237356a-5e32-4ddb-bac7-4e521a56326e", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ce72a656-73d8-49c9-a04e-cfefd0e89d13", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "c5fa6724-d145-484d-8f33-dafa65b13b74", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "d5324b44-3656-4978-bb13-96915cdf60ff", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ef01e291-c2c5-4d47-bc83-e50a1971ee55", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "34d178dc-ed4b-4517-b261-de55f93c7ab9", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "de1f054b-15ad-4839-a54e-537039a31820", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-clients", + "manage-clients", + "query-clients", + "view-authorization", + "view-realm", + "manage-events", + "create-client", + "query-realms", + "view-events", + "view-identity-providers", + "view-users", + "query-users", + "manage-authorization", + "manage-identity-providers", + "impersonation", + "manage-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5df84e42-5988-4b59-a8eb-b83835f1f662", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "d66bcb04-700d-483c-9628-207d0bd431bd", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "0b06878c-6957-40d0-8783-23e075fcf103", + "attributes": {} + } + ], + "account": [ + { + "id": "044b18e6-b560-43c8-925d-80fbce6ddc28", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "fa040d5d-ca2f-4386-85bd-75716818b4a0", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "76c4a106-bd5d-43b2-9098-708ea5813f2a", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} }, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "b48d559d-9f77-4106-afef-8d9ba7c62e2f", - "name" : "view-groups", - "description" : "${role_view-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "49dce239-8341-44bf-8a1b-09b9992da150", - "name" : "view-profile", - "description" : "${role_view-profile}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - } ] + { + "id": "c99f7465-dd93-4f8d-ab26-64439d75f0c2", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "43c296b2-6d12-41f8-898a-2663dde41447", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "b48d559d-9f77-4106-afef-8d9ba7c62e2f", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "49dce239-8341-44bf-8a1b-09b9992da150", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + } + ] } }, - "groups" : [ ], - "defaultRole" : { - "id" : "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name" : "default-roles-shipflow", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae" + "groups": [], + "defaultRole": { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae" }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], - "localizationTexts" : { }, - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyExtraOrigins" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessExtraOrigins" : [ ], - "users" : [], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clientScopeMappings" : { - "account" : [ { - "client" : "account-console", - "roles" : [ "manage-account", "view-groups" ] - } ] + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "0c6a758d-afe4-47a4-9f09-df82c6e99653", + "username": "master", + "emailVerified": true, + "createdTimestamp": 1775150577528, + "enabled": true, + "totp": false, + "credentials": [ + { + "id": "eef9ff22-fc19-48b5-83c4-7036ffa3c7bb", + "type": "password", + "userLabel": "My password", + "createdDate": 1775150577528, + "secretData": "{\"value\":\"xhuravFV0LOqcWNYBRWJ8fvnc2HVu0KvUwmDouoIYzU=\",\"salt\":\"GE9MOvjB9bf+/tpUY4QNOQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "MASTER" + ], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] }, - "clients" : [ { - "id" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/shipflow/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/shipflow/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" + "clients": [ + { + "id": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "d944f492-235f-4a2e-8713-5f4893969dcb", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/shipflow/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/shipflow/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + { + "id": "d944f492-235f-4a2e-8713-5f4893969dcb", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "b5c354ab-6b26-437a-960b-3aacde70b086", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "b5c354ab-6b26-437a-960b-3aacde70b086", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "baa30b35-5489-4baf-85e0-a05059153a81", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "0b06878c-6957-40d0-8783-23e075fcf103", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "clientId": "shipflow-api", - "name": "${login-client-id}", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/*" - ], - "webOrigins": [ - "/*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "login_theme": "", - "display.on.consent.screen": "false", - "consent.screen.text": "", - "frontchannel.logout.url": "", - "backchannel.logout.url": "" + { + "id": "baa30b35-5489-4baf-85e0-a05059153a81", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true + { + "id": "0b06878c-6957-40d0-8783-23e075fcf103", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "authorizationServicesEnabled": false - },{ - "id" : "39987a2a-1607-481c-a891-e33a14c9c337", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/shipflow/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/shipflow/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + { + "id": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "9e3dc333-40f0-457e-b5e7-fdadd440aa78", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - } ], - "clientScopes" : [ { - "id" : "80461e14-093c-4647-b9fb-b7a8fc843ff2", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" + { + "clientId": "shipflow-api", + "name": "${login-client-id}", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "login_theme": "", + "display.on.consent.screen": "false", + "consent.screen.text": "", + "frontchannel.logout.url": "", + "backchannel.logout.url": "" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + }, + "authorizationServicesEnabled": false }, - "protocolMappers" : [ { - "id" : "abdac8d3-83eb-4993-9c0a-57221ffc55b4", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" - } - }, { - "id" : "33d17aeb-10a0-4f8c-9e6c-afdf595da401", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "17861556-524a-4d33-9e30-d7df5297f61e", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${profileScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "39987a2a-1607-481c-a891-e33a14c9c337", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/shipflow/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/shipflow/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9e3dc333-40f0-457e-b5e7-fdadd440aa78", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "80461e14-093c-4647-b9fb-b7a8fc843ff2", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "abdac8d3-83eb-4993-9c0a-57221ffc55b4", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "33d17aeb-10a0-4f8c-9e6c-afdf595da401", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] }, - "protocolMappers" : [ { - "id" : "c88cf316-27ec-4940-9b87-614a6125ee3a", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "828bd040-4877-452e-b083-3a658a52853c", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - }, { - "id" : "341a7f66-f7fc-4cac-8584-77dc6444e259", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - }, { - "id" : "6415f321-b218-401d-88c0-492b428cd86c", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "b971d53b-9925-4ffe-a649-4aab9ed3d880", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "09e729d0-d8dc-4693-a693-37d3237aadcd", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - }, { - "id" : "8123f18d-17b8-454e-9eb9-493e3ecf988b", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "041d5689-35c8-4776-ba95-96ca02ac2b2f", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "e800e5a6-90b5-4987-944e-5226110af8cf", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "26f011c7-5681-4e8e-8f1a-d171495b2639", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - }, { - "id" : "dde63b52-45c9-4587-9341-b5e5fe4e75a2", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "f941894d-e133-44ee-8747-698c9b3ffa76", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "d15e02e3-4763-4519-a622-365a2840d37f", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "3d56cee0-2f92-4161-b112-affc02423932", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${phoneScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "17861556-524a-4d33-9e30-d7df5297f61e", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "c88cf316-27ec-4940-9b87-614a6125ee3a", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "828bd040-4877-452e-b083-3a658a52853c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "341a7f66-f7fc-4cac-8584-77dc6444e259", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "6415f321-b218-401d-88c0-492b428cd86c", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "b971d53b-9925-4ffe-a649-4aab9ed3d880", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "09e729d0-d8dc-4693-a693-37d3237aadcd", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "8123f18d-17b8-454e-9eb9-493e3ecf988b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "041d5689-35c8-4776-ba95-96ca02ac2b2f", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e800e5a6-90b5-4987-944e-5226110af8cf", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "26f011c7-5681-4e8e-8f1a-d171495b2639", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "dde63b52-45c9-4587-9341-b5e5fe4e75a2", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "f941894d-e133-44ee-8747-698c9b3ffa76", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "d15e02e3-4763-4519-a622-365a2840d37f", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + } + ] }, - "protocolMappers" : [ { - "id" : "18102df6-716f-4670-b62e-f7c072e3b8c5", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : true, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "09913ba8-d796-4445-ad45-e8c13d6b3f6e", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "ab9eb324-487d-44a3-9a61-d05f989bc13b", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "", - "display.on.consent.screen" : "false" + { + "id": "3d56cee0-2f92-4161-b112-affc02423932", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "18102df6-716f-4670-b62e-f7c072e3b8c5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": true, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "09913ba8-d796-4445-ad45-e8c13d6b3f6e", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] }, - "protocolMappers" : [ { - "id" : "e7480737-d23a-4057-ac0b-4527d5747338", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "148965f8-21a6-4eb0-a062-3c7138f2c351", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" + { + "id": "ab9eb324-487d-44a3-9a61-d05f989bc13b", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e7480737-d23a-4057-ac0b-4527d5747338", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] }, - "protocolMappers" : [ { - "id" : "674690ff-249d-434e-8c01-cdc901c02360", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true" + { + "id": "148965f8-21a6-4eb0-a062-3c7138f2c351", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "674690ff-249d-434e-8c01-cdc901c02360", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "b40f77db-de92-4c75-b79b-10506658a17f", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" } - } ] - }, { - "id" : "b40f77db-de92-4c75-b79b-10506658a17f", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "e3ce3daa-b344-4d67-8ff8-374157a39321", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "${rolesScopeConsentText}", - "display.on.consent.screen" : "true" }, - "protocolMappers" : [ { - "id" : "e82a1e72-897a-4b29-9138-6047db2d2d55", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" + { + "id": "e3ce3daa-b344-4d67-8ff8-374157a39321", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e82a1e72-897a-4b29-9138-6047db2d2d55", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "677bda21-6981-4dff-ab3d-2d17011fa95f", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "81d25232-91dc-4ceb-903d-46b2d0953ea2", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "a44b439c-7386-4fee-849f-d7bbc0ce44ba", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "d2f34d78-2c96-436e-ac6b-05f34a46de36", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "fea6c421-3e54-499f-9189-38d992452d28", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "9394eaaf-125a-4df9-93d4-c7a309a81606", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + }, + { + "id": "d067706a-9d23-456b-acb1-253f62f663c5", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "45cf6edb-f05e-463d-89f0-95dfef49b8e3", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "838220c4-f89b-491b-a1fb-4cf0e9e9be80", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "649a9fd6-d43f-4adf-bc06-fdd97ef6b66e", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "925c2991-c100-4031-b582-6aeb21a4be6d", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "9862faf6-ef7f-4351-901f-f69c4d03e177", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "931e5f0e-94b5-4990-88a8-b2e3e37b715e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "700ee54a-d541-4f30-80c5-feed1a278050", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "d68d4e22-9119-4813-a40c-b72f2018917a", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } } - }, { - "id" : "677bda21-6981-4dff-ab3d-2d17011fa95f", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String", - "multivalued" : "true" + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "c418f251-7d0a-4f89-8020-8e3c3a7fa0b3", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":4,\"max\":10},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[],\"edit\":[]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[],\"edit\":[]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + ] + } } - }, { - "id" : "81d25232-91dc-4ceb-903d-46b2d0953ea2", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String", - "multivalued" : "true" + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "3c2fef2f-cebe-49f9-bd02-67d506bc8724", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "430be029-2d11-4edf-bf33-cc6b13d6066d" + ], + "secret": [ + "45L5ziXkEQsQjxvtdSpRIA" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "fca8ba49-3626-4e1f-abaa-298ec4775dbf", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEogIBAAKCAQEAv07QFoU7dJcNIeyAPE0u1y2colgiVHNeoaNcrCsQhfFP11qqRXrPOZL8reKEAFFrC8nHyc7ZjVSjUMUGzcyqpHDxuXM0D2lQGz5OAShfY4QGkbrzqyPBtmhsr8vehnhRWUipNgEP4klZaIOac8rXT7p42fIzKk4fDK6HUTPVcWjphhHcY5tZdLcHYUF4BtFJn5I++DBW5Xkg+cWmd/jX6BVlP84ATYSXnxNDDJx1N+mJ8dnUsjYmt79rCLqwgTVpBMKbIpf9LuIbNTOHm6fV4BN44fDrxMm/xMzT1S0bAUrAhMctTF5/jBw4g/TqVIgkcfcttkik5AfOT84p2t4TKQIDAQABAoIBAAWnNe+2be548LjICPWPN9NAvCxRc6lAdANNhSVqy90TZ4829Qli0skSIoCebRVn0oSZiLt6TvQ11DIkslnmQoQjuMboxDjw3R7C+XHgECqMChgoGL99yeGCpjSPLxMU7t13L9XiU+Z1Uaysl+W0UKbA3UWeejvVrWX16daA1D26xXTqTTR11YbhlgBQxKGQXFYXm9IORNYevAYRSdIl6LiDHHb/NgsgNMF7zVXoiLs87s8+sEZ5XYoJJDCoDMDnXLqUlTyKqouVz3iU5bL8fHHdWO7AfwcbfgiAGHe5Ffskm62k153Ye88R3zYRaAeaifjTHfYrNLFCZY5unZDc4BECgYEA59fNhjcPxDlIAmkb4K028qEAed7RFxNUHcQ5jSScG2OrGr3dVzWdE0I+8u3yn/5PCpVglMSfndr9A3Xu+jttJ5WmNn9QnoqeGiWZsp7GXV9mqbbobPKHzO9Mu9QbVJu6+ZkDUaFaryoOI3zNzLUOAsXN8Vc9ved0vPduAuLryjECgYEA0z3F8sQ6BjEoWNbujuVHI+Sms0gOUIIDJzlF/NB3CvLZGe4yJ5pf7G+omVMTspaxNg48E6PO4NjWAThQW9hZBI1bgR/hKOghbeqF2dP+UEEepoo8KSY6VEIPUxEjyrIWTX0jDJHNnNr72j60wnHHv9Z7laj2qafYo9ECFWxhInkCgYA+xV8QB7htGFU20d6KZluKNa07UeiqpsEPjiFG5bKed83L37wd8JYmsLj6bRJT3zbnVqpfnRzaUIBQf43EknJrVUk7WB0rz7weuC90/SgX/8x8BtnHJaM/CUttT3BW6BMnoRYU8+rpoilR0mimFB9HAOdRgJ1m3VPuFc/jWC0fAQKBgBWH/l04UxG+gPZNMhOumwm1jKhJd+wM1HVzCQcz2G5tQmO6O7J9sblPyEeYiDFz2qw/1y/JSpTwhR+qtcYmzyv/nIwUy8Z3orCpbus9CHb1rEIdZPRsyRU9hoJZBOTsMgnD74agdey/BVzBd3s6TbnoCsC+cCXqzdIkw6mbWmtBAoGAOzbfy/jDwIfpNEoHqHG0IwIbpvked3VAMN64UDXVQJ82M9JcTyqeWl3d89h6YGaOoBAZf4+W3X+V+WNu5ZKgdgmFR8PVyibUVWCZ97KjjnM2tEz4+ovfNC5PiVgDQokpOi6qGCPWresVPK4NSEksoYhpv/I+l3WsifXKUU0hy9c=" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIICnzCCAYcCBgGdVEmoPTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhzaGlwZmxvdzAeFw0yNjA0MDMxNjU4MjZaFw0zNjA0MDMxNzAwMDZaMBMxETAPBgNVBAMMCHNoaXBmbG93MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv07QFoU7dJcNIeyAPE0u1y2colgiVHNeoaNcrCsQhfFP11qqRXrPOZL8reKEAFFrC8nHyc7ZjVSjUMUGzcyqpHDxuXM0D2lQGz5OAShfY4QGkbrzqyPBtmhsr8vehnhRWUipNgEP4klZaIOac8rXT7p42fIzKk4fDK6HUTPVcWjphhHcY5tZdLcHYUF4BtFJn5I++DBW5Xkg+cWmd/jX6BVlP84ATYSXnxNDDJx1N+mJ8dnUsjYmt79rCLqwgTVpBMKbIpf9LuIbNTOHm6fV4BN44fDrxMm/xMzT1S0bAUrAhMctTF5/jBw4g/TqVIgkcfcttkik5AfOT84p2t4TKQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBaTG52wKWCIO6pkkczu39V+xvsItyyW1PaZjXOWMTzN6jhaYgy1cIQhRf73HVoOyscL23qzbpAe5tSe4NAhuKpuhpewLJ4uifW7vUG1flsDpz+HVU4X96Se7W0RBfOp1pYBwl1CCzT3a94zeBAT0g77b6f6vBB3UUtDi1ONUjxuguI+/7PMIq3UDp49I0D9G/Za8xm4Hl0dXpGCorx77VabBicFU2i5aHLaFQz9uvL5eIoKckutXF9GSoY9vNXpa+Qjcq6wMHsFBI/1veR5iZ31fGHYYYI375VdxiBzqEF8eGuUTf5Ef8ZfnAMdm5HFt09buFX6NOQmVaVogUHFJ1e" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "75ea9636-6875-4464-8066-4d961fb32d6e", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "17b83a53-8d14-40a1-b4f2-f895c7489b67" + ], + "secret": [ + "TGPGAJeWFZdtUsJyieamyDJUzMzuiD_KaHdBftdfjQ_6H53rUdgiWIcakE9AOyklFaxc64j-40vKa0qtLWB28OwJ-jYIFgULDg4QZGAHXbZJ12hbf5_NXog5UFQVlB3TkLJ2HNbLgGnJ_eWnyzsVJfZkgvS8t-uhHVjDf9XN6Jo" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "82f584db-f4b2-4fbf-974f-b15ef6a7a6c6", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEA3qlBybBF++h5Q/U6o8IS8Ld2YT23MTFLO6h3ZKqzYLsp55rhg4FMoYEIU7L57PCmtkCr4ON/04dayw2OHBPi4UvltK8HTOZnri6j3ACO2SkxO7iV0UwRmjMzYH+rB33OAr6qn/pg2dQp04NHTzgixvvca9y1G4DR1EpnsePQRTRz3INWT+KYokL4INdubvtGdsmv/PedczzXkPSeOWKj/tA7epD0IV0apnDg5vrpgaARGF30zwLYGdWp3pZbmggkXhHLHijUctzEEViKe9YJtS6gH9q329XGT2RCNhIwthPPYV5+eSdsRuIpZ5AXQN+j91FKdBCdVo1lKlbsue0h4QIDAQABAoIBABfeCshE1eufysfnFIcTOZaGA/F+fRGP2CGn+ExZI+s9hMtnxb6j8IPrYeoe6D1mumgU3Je5qy0QeEIFzPLjitFdolzQ2jZ7CCgapcPiZ22NxdJCAgUyYzylOl+gr8OYz6lpqL6HRzRyUp1ymAU83jV4L/N78AnnsBZSd3URF3UjbAS6z6NQfoqDCOxmXiA/iLYGmpB+SQKbjWaxeozsnlgwCbH6EER/aQVlX8vAmt5bFbh3dTAL9pktQFxW9P/LIwUfd1ghEutaNr1r52UEU7w+KyiDDLHaT9Tt5piO9fhMNgkDxtRl3W+PVIt5/nhaQPLcfE4NTnRBlsGzA+a/P9UCgYEA9z7G83QarH90A+HcFkKw+/IT72yZdukN7V+p4W0DSxQX57n5WTy46+sMTbsGdnvCJSP1iOXEGyFQhc8IrrFJXKF9K5gtzQ1dvnLQCwvhaP5awBTS4qm/5dnlOUVW0770mlHucvPVzwgRBz7GEDKbd8xItL+kHB0YvPekI5bwd1MCgYEA5ouheqZtFF3ufxJLUFolTf8RfWYsm8Z5G+O2fEhW62r6ujmDXwk9eNxSRj1SoJzFnNIS3rgd4Y1/XRHfw+mmvFuFC7Ih1ktmPQ9LpcbE02tvIwings4zHTv3Fv1t+qMnk54AuPmT3txTHZx4eVKtOMXIqSUT+HCW+/dOsi/0X3sCgYEAgf4eqjecIp+sRrJEfeu4k+62LobBtTRZXzmR3vTq61l4LByqjhGQBHIDeQbhIgB1lgNu//gWAFGmvYOZxAdwU+SQJBCR3CKv7Ab/fR9U91fsLNuF+ShYvaevjkn3mcLnZg+3t/adrolGMrH9ftyswvLEM0wjI6jkrc3iHdgpPAMCgYEArIB74fbXFW83PfNlUQkycorQ/mBOLnyyL9ERwSqrhtj0JBVWm+yhB2brVM0bnzvOjQmOvwFKsnMagnwWT1Prw3JDOb4enWaraDKiqrbwnTT84lzeYfyBuHUe7B/Sg8BCo6yM49sy7oUy16w1ZKodHKa4/v7UU4eDIaMpSiChnDMCgYAk3Yt2AFbp3hmH9atKa563tZB9niuRVsQKpE2d+/3l2H5H+iRnXXHMSgtix52Cq3F8M3AzS27lzg6bT95B+YBOqYIkNlr9kjAY0rscR0yELNWeRLXeOFVFn9EJEJ7RRtzi1ttnngz4WjdcbFNWoDSqJI6micgCyfhcGU+g9ybHkw==" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIICnzCCAYcCBgGdVEmpxzANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhzaGlwZmxvdzAeFw0yNjA0MDMxNjU4MjZaFw0zNjA0MDMxNzAwMDZaMBMxETAPBgNVBAMMCHNoaXBmbG93MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3qlBybBF++h5Q/U6o8IS8Ld2YT23MTFLO6h3ZKqzYLsp55rhg4FMoYEIU7L57PCmtkCr4ON/04dayw2OHBPi4UvltK8HTOZnri6j3ACO2SkxO7iV0UwRmjMzYH+rB33OAr6qn/pg2dQp04NHTzgixvvca9y1G4DR1EpnsePQRTRz3INWT+KYokL4INdubvtGdsmv/PedczzXkPSeOWKj/tA7epD0IV0apnDg5vrpgaARGF30zwLYGdWp3pZbmggkXhHLHijUctzEEViKe9YJtS6gH9q329XGT2RCNhIwthPPYV5+eSdsRuIpZ5AXQN+j91FKdBCdVo1lKlbsue0h4QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA5KjW40V6ORkYrbRUDqADxkFePlnRhABnlZREXB2EFcnDjOUFxDlUVlG3j/MLw/EPSbf7HMEk6ZVeBKo7hG3DXb1eYEgob7mgIlkajw7YjiVnNBc0fg/UtpV6qkCGPIPvNMEUIAk2v1wuDIRM82020gmcJWW1nGIh48W9sGIszoelYnPwEq8KeMGljBGvw919lIn7VwzHn5NSf640sI9RHtBw/GOjduV55kGdWYSpcLFjLT/qRC4aejDI+KikW52qdekEhx8iTpbCDigijlViNjlbKSqrfhnYdUNPOxwpff2okE85bSYhejpmPE9zdd/v7CPcQWv0MZOed4zQHLIY3" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } } - } ] - }, { - "id" : "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${addressScopeConsentText}", - "display.on.consent.screen" : "true" + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "fda8b12c-9bb9-4c04-860a-5b69202d5714", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "a44b439c-7386-4fee-849f-d7bbc0ce44ba", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "introspection.token.claim" : "true", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - }, { - "id" : "d2f34d78-2c96-436e-ac6b-05f34a46de36", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "ff955351-02a7-4978-9dc8-3a7c3e46fb91", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "fea6c421-3e54-499f-9189-38d992452d28", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" - } - } ] - }, { - "id" : "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${emailScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "f778c69e-8c53-427c-a299-5d49906aea63", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "9394eaaf-125a-4df9-93d4-c7a309a81606", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", - "name" : "basic", - "description" : "OpenID Connect scope for add all basic claims to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" + { + "id": "d3d5c34e-35cd-49aa-8e03-90dce67080ee", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "9eaf459c-709f-46d7-b312-bb3d01bf561e", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8a6ccaa6-96a0-45a7-af86-69238b8762ea", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "a726d5c4-022e-475b-bf0d-57e9dcb911fa", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", - "name" : "auth_time", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "AUTH_TIME", - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "auth_time", - "jsonType.label" : "long" + { + "id": "86523e55-21bf-44c9-b107-6f853f0f168a", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "ceac3589-19df-47f7-ad07-2077ea1fc5d4", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ba752550-cfc6-48b3-a521-4972685f42b2", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "ed913470-af37-429c-af89-cc1397fb4147", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "295facf5-3aca-4b02-a0cf-360593092a46", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "514b1696-d644-4f01-8b96-e6013de7078c", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "611045a9-bade-435d-b549-b62e497d44a5", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "74141972-4429-4565-b576-26ee4b374060", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "039a1f5a-d31b-4e4e-a401-e02694d1cb26", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" } - }, { - "id" : "d067706a-9d23-456b-acb1-253f62f663c5", - "name" : "sub", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-sub-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" + }, + { + "id": "8d580f89-f9cd-4259-ab24-affe163ff5e7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "basic" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection" : "1; mode=block", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "fda8b12c-9bb9-4c04-860a-5b69202d5714", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "ff955351-02a7-4978-9dc8-3a7c3e46fb91", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "f778c69e-8c53-427c-a299-5d49906aea63", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "d3d5c34e-35cd-49aa-8e03-90dce67080ee", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", - "alias" : "Handle Existing Account", - "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "9eaf459c-709f-46d7-b312-bb3d01bf561e", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "8a6ccaa6-96a0-45a7-af86-69238b8762ea", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "a726d5c4-022e-475b-bf0d-57e9dcb911fa", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "86523e55-21bf-44c9-b107-6f853f0f168a", - "alias" : "browser", - "description" : "browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "ceac3589-19df-47f7-ad07-2077ea1fc5d4", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "ba752550-cfc6-48b3-a521-4972685f42b2", - "alias" : "first broker login", - "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - } ] - }, { - "id" : "ed913470-af37-429c-af89-cc1397fb4147", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "295facf5-3aca-4b02-a0cf-360593092a46", - "alias" : "registration", - "description" : "registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "514b1696-d644-4f01-8b96-e6013de7078c", - "alias" : "registration form", - "description" : "registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-terms-and-conditions", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 70, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "611045a9-bade-435d-b549-b62e497d44a5", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "74141972-4429-4565-b576-26ee4b374060", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "039a1f5a-d31b-4e4e-a401-e02694d1cb26", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" } - }, { - "id" : "8d580f89-f9cd-4259-ab24-affe163ff5e7", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": false, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : false, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "VERIFY_PROFILE", - "name" : "Verify Profile", - "providerId" : "VERIFY_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 90, - "config" : { } - }, { - "alias" : "delete_credential", - "name" : "Delete Credential", - "providerId" : "delete_credential", - "enabled" : true, - "defaultAction" : false, - "priority" : 100, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "firstBrokerLoginFlow" : "first broker login", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaAuthRequestedUserHint" : "login_hint", - "oauth2DevicePollingInterval" : "5", - "clientOfflineSessionMaxLifespan" : "0", - "clientSessionIdleTimeout" : "0", - "clientOfflineSessionIdleTimeout" : "0", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false", - "cibaExpiresIn" : "120", - "oauth2DeviceCodeLifespan" : "600", - "parRequestUriLifespan" : "60", - "clientSessionMaxLifespan" : "0", - "organizationsEnabled" : "false" + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "organizationsEnabled": "false" }, - "keycloakVersion" : "25.0.6", - "userManagedAccessAllowed" : false, - "organizationsEnabled" : false, - "clientProfiles" : { - "profiles" : [ ] + "keycloakVersion": "25.0.6", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "clientProfiles": { + "profiles": [] }, - "clientPolicies" : { - "policies" : [ ] + "clientPolicies": { + "policies": [] } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java new file mode 100644 index 0000000..0900f5b --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java @@ -0,0 +1,148 @@ +package com.shipflow.notificationservice.application; + +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class NotificationOrchestratorService { + + private static final String DEFAULT_WORKING_HOURS = "09:00 ~ 18:00"; + private static final String UNKNOWN = "확인 필요"; + + private final AiAppService aiAppService; + private final SlackAppService slackAppService; + private final AiLogRepository aiLogRepository; + + @Transactional + public void handleShipmentCreated(ShipmentCreatedEvent event) { + GenerateDeadlineCommand command = toGenerateDeadlineCommand(event); + + AiLogResult aiResult = aiAppService.generateAiLog(command); + + String slackMessage = createDeadlineSlackMessage(command, aiResult); + + try { + slackAppService.sendSlackMessage( + new SendSlackMessageCommand( + event.getReceiverSlackId(), + command.relatedShipmentId(), + aiResult.aiId(), + slackMessage, + SlackMessageType.DEADLINE_ALERT + ) + ); + + markSlackSendSuccess(aiResult.aiId()); + + } catch (Exception e) { + markSlackSendFail(aiResult.aiId()); + throw e; + } + } + + private GenerateDeadlineCommand toGenerateDeadlineCommand(ShipmentCreatedEvent event) { + return new GenerateDeadlineCommand( + event.getShipmentId(), // relatedShipmentId + null, // shipmentManagerId + extractFromHub(event), // fromHub + extractToHub(event), // toHub + java.util.Collections.emptyList(), // route + extractProductText(event), // product + extractRequestNote(event), // requestNote + event.getRequestDeadline(), // deadline + DEFAULT_WORKING_HOURS, // workingHours + AiRequestType.DEADLINE, // requestType + null // workDate + ); + } + + private String extractFromHub(ShipmentCreatedEvent event) { + return event.getDepartureHubId() == null + ? UNKNOWN + : "허브ID: " + event.getDepartureHubId(); + } + + private String extractToHub(ShipmentCreatedEvent event) { + return event.getArrivalHubId() == null + ? UNKNOWN + : "허브ID: " + event.getArrivalHubId(); + } + + private String extractProductText(ShipmentCreatedEvent event) { + String productId = event.getProductId() == null ? UNKNOWN : event.getProductId().toString(); + String quantity = event.getQuantity() == null ? "수량 미확인" : event.getQuantity() + "개"; + + return "상품ID: " + productId + " / 수량: " + quantity; + } + + private String extractRequestNote(ShipmentCreatedEvent event) { + return (event.getRequestNote() == null || event.getRequestNote().isBlank()) + ? "없음" + : event.getRequestNote(); + } + + private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLogResult aiResult) { + String routeText = (command.route() == null || command.route().isEmpty()) + ? "없음" + : String.join(" → ", command.route()); + + String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) + ? "없음" + : command.requestNote(); + + return """ + 🚚 배송 요청 알림 + + 상품 정보: %s + 요청 사항: %s + + 발송지: %s + 경유지: %s + 도착지: %s + + ⏰ AI 계산 최종 발송 시한: %s + + ※ 해당 시간 이전에 발송을 완료해주세요. + """.formatted( + command.product(), + requestNote, + command.fromHub(), + routeText, + command.toHub(), + aiResult.finalDeadlineAt() + ); + } + + private void markSlackSendSuccess(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + + aiLog.markSendSuccess(); + } + + private void markSlackSendFail(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + + aiLog.markSendFail(); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index bd4fe9a..fb05f75 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -10,15 +10,12 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; -import com.shipflow.notificationservice.application.slack.SlackAppService; -import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.domain.ai.AiGenerator; import com.shipflow.notificationservice.domain.ai.AiLog; import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import com.shipflow.notificationservice.domain.ai.vo.AiResponseInfo; -import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; import lombok.RequiredArgsConstructor; @@ -31,7 +28,6 @@ public class AiAppService { private final AiLogRepository aiLogRepository; private final AiGenerator aiGenerator; - private final SlackAppService slackAppService; //테스트용 외부(AI만 실행) @Transactional @@ -68,35 +64,6 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { } } - //슬랙으로 전송 - //AI 실행 → 결과로 Slack 발송 → sendStatus 업데이트 - @Transactional - public AiLogResult generateAndSendSlack(GenerateDeadlineCommand command, String receiverSlackId) { - - AiLogResult aiResult = generateAiLog(command); - - String slackMessage = createDeadlineSlackMessage(command, aiResult); - - try { - slackAppService.sendSlackMessage( - new SendSlackMessageCommand( - receiverSlackId, - command.relatedShipmentId(), - aiResult.aiId(), - slackMessage, - SlackMessageType.DEADLINE_ALERT - ) - ); - - markSlackSendSuccess(aiResult.aiId()); - return aiResult; - - } catch (Exception e) { - markSlackSendFail(aiResult.aiId()); - throw e; - } - } - public AiLogResult getAiLog(UUID aiId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); @@ -170,54 +137,4 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { workingHours ); } - - // Slack 메시지용 (사용자에게 보여줄 출력) - private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLogResult aiResult) { - - String routeText = (command.route() == null || command.route().isEmpty()) - ? "없음" - : String.join(" → ", command.route()); - - String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) - ? "없음" - : command.requestNote(); - - return """ - 🚚 배송 요청 알림 - - 상품 정보: %s - 요청 사항: %s - - 발송지: %s - 경유지: %s - 도착지: %s - - ⏰ AI 계산 최종 발송 시한: %s - - ※ 해당 시간 이전에 발송을 완료해주세요. - """.formatted( - command.product(), - requestNote, - command.fromHub(), - routeText, - command.toHub(), - aiResult.finalDeadlineAt() - ); - } - - // Slack 성공 시 AiLog 상태 업데이트 - private void markSlackSendSuccess(UUID aiLogId) { - AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) - .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); - - aiLog.markSendSuccess(); - } - - // Slack 실패 시 AiLog 상태 업데이트 - private void markSlackSendFail(UUID aiLogId) { - AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) - .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); - - aiLog.markSendFail(); - } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java index 6b7468e..884600c 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java @@ -3,30 +3,29 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; +import com.shipflow.notificationservice.application.NotificationOrchestratorService; import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Component +@RequiredArgsConstructor public class ShipmentCreatedConsumer { + private final NotificationOrchestratorService notificationOrchestratorService; + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) public void handleShipmentCreated(ShipmentCreatedEvent event) { log.info( - "[ShipmentCreatedConsumer] shipment.created 수신 - orderId={}, ordererId={}, supplierCompanyId={}, receiverCompanyId={}, productId={}, quantity={}, departureHubId={}, arrivalHubId={}, requestDeadline={}, requestNote={}, occurredAt={}", + "[ShipmentCreatedConsumer] shipment.created 수신 - shipmentId={}, orderId={}, receiverSlackId={}", + event.getShipmentId(), event.getOrderId(), - event.getOrdererId(), - event.getSupplierCompanyId(), - event.getReceiverCompanyId(), - event.getProductId(), - event.getQuantity(), - event.getDepartureHubId(), - event.getArrivalHubId(), - event.getRequestDeadline(), - event.getRequestNote(), - event.getOccurredAt() + event.getReceiverSlackId() ); + + notificationOrchestratorService.handleShipmentCreated(event); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index cfe940d..3eb2445 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -10,14 +10,15 @@ @Getter @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ShipmentCreatedEvent { - @JsonIgnoreProperties(ignoreUnknown = true) private UUID shipmentId; private UUID orderId; private UUID ordererId; private UUID supplierCompanyId; private UUID receiverCompanyId; + private String receiverSlackId; private UUID productId; private Integer quantity; private UUID departureHubId; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index c58d33f..9faccbf 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -12,7 +12,6 @@ import com.shipflow.common.exception.ApiResponse; import com.shipflow.notificationservice.application.ai.AiAppService; -import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; import com.shipflow.notificationservice.presentation.common.BasePageResponse; @@ -41,20 +40,6 @@ public ApiResponse generateAiLog( ); } - // TODO: 이벤트 기반 처리 완료 후 삭제 예정 (AI → Slack 테스트용) - // 임시 컨트롤러 - @PostMapping("/test-slack") - public ApiResponse testSlack( - @Valid @RequestBody GenerateDeadlineRequest request - ) { - AiLogResult result = aiAppService.generateAndSendSlack( - request.toCommand(), - request.receiverSlackId() - ); - - return ApiResponse.ok(AiLogResponse.from(result)); - } - @GetMapping("/{aiId}") public ApiResponse getAiLog(@PathVariable UUID aiId) { return ApiResponse.ok( From 8e8cfed44b1303e60fcd3a9aa22f532182641001 Mon Sep 17 00:00:00 2001 From: 250 Date: Mon, 6 Apr 2026 14:41:44 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat(notification):=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D/=EC=9D=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipflow/common/domain/BaseEntity.java | 8 ++ .../NotificationOrchestratorService.java | 41 ++++++--- .../application/ai/AiAppService.java | 36 +++++--- .../dto/command/GenerateDeadlineCommand.java | 18 +++- .../ai/dto/command/SearchAiLogCommand.java | 20 +++++ .../application/slack/SlackAppService.java | 67 +++++++++----- .../command/SearchSlackMessageCommand.java | 18 ++++ .../dto/command/SendSlackMessageCommand.java | 2 + .../command/UpdateSlackMessageCommand.java | 2 + .../notificationservice/domain/ai/AiLog.java | 6 +- .../domain/ai/exception/AiErrorCode.java | 1 + .../domain/ai/repository/AiLogRepository.java | 3 +- .../domain/slack/SlackMessage.java | 25 +++--- .../slack/exception/SlackErrorCode.java | 1 + .../repository/SlackMessageRepository.java | 3 +- .../messaging/dto/ShipmentCreatedEvent.java | 2 - .../persistence/ai/AiLogJpaRepository.java | 5 -- .../persistence/ai/AiLogRepositoryImpl.java | 64 +++++++++++++- .../slack/SlackMessageJpaRepository.java | 5 -- .../slack/SlackMessageRepositoryImpl.java | 62 ++++++++++++- .../dto/request/GenerateDeadlineRequest.java | 3 +- .../ai/external/AiController.java | 69 ++++++++++++--- .../presentation/common/BasePageRequest.java | 8 +- .../dto/request/SendSlackMessageRequest.java | 6 +- .../request/UpdateSlackMessageRequest.java | 8 +- .../slack/external/SlackController.java | 88 +++++++++++++++---- .../SlackAppServiceTest.java | 66 +++++++++++--- 27 files changed, 512 insertions(+), 125 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index ff37f32..8e1a554 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -42,6 +42,14 @@ public abstract class BaseEntity { protected UUID deletedBy; + protected void markCreated(UUID userId) { + this.createdBy = userId; + } + + protected void markUpdated(UUID userId) { + this.updatedBy = userId; + } + protected void softDelete(UUID userId) { this.deletedAt = LocalDateTime.now(); this.deletedBy = userId; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java index 0900f5b..5737c1c 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java @@ -1,5 +1,6 @@ package com.shipflow.notificationservice.application; +import java.util.Collections; import java.util.UUID; import org.springframework.stereotype.Service; @@ -43,6 +44,8 @@ public void handleShipmentCreated(ShipmentCreatedEvent event) { try { slackAppService.sendSlackMessage( new SendSlackMessageCommand( + event.getOrdererId(), + "MASTER", // TODO: 내부 시스템 자동 발송 권한 처리 방식 확정 후 변경 event.getReceiverSlackId(), command.relatedShipmentId(), aiResult.aiId(), @@ -61,17 +64,31 @@ public void handleShipmentCreated(ShipmentCreatedEvent event) { private GenerateDeadlineCommand toGenerateDeadlineCommand(ShipmentCreatedEvent event) { return new GenerateDeadlineCommand( - event.getShipmentId(), // relatedShipmentId - null, // shipmentManagerId - extractFromHub(event), // fromHub - extractToHub(event), // toHub - java.util.Collections.emptyList(), // route - extractProductText(event), // product - extractRequestNote(event), // requestNote - event.getRequestDeadline(), // deadline - DEFAULT_WORKING_HOURS, // workingHours - AiRequestType.DEADLINE, // requestType - null // workDate + event.getOrderId(), + event.getOrdererId(), + null, // relatedShipmentId: 현재 이벤트에 없으면 추후 보강 + null, // shipmentManagerId: 현재 이벤트에 없으면 추후 보강 + event.getReceiverSlackId(), + + event.getSupplierCompanyId(), + event.getReceiverCompanyId(), + + event.getProductId(), + extractProductText(event), + event.getQuantity(), + + event.getDepartureHubId(), + extractFromHub(event), + event.getArrivalHubId(), + extractToHub(event), + Collections.emptyList(), // route: 허브 내부 API 연동 전까지 기본값 + + extractRequestNote(event), + event.getRequestDeadline(), + DEFAULT_WORKING_HOURS, + + AiRequestType.DEADLINE, + null ); } @@ -112,6 +129,7 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog return """ 🚚 배송 요청 알림 + 주문 번호: %s 상품 정보: %s 요청 사항: %s @@ -123,6 +141,7 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog ※ 해당 시간 이전에 발송을 완료해주세요. """.formatted( + command.orderId(), command.product(), requestNote, command.fromHub(), diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index fb05f75..119608a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -9,6 +9,7 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; import com.shipflow.notificationservice.domain.ai.AiGenerator; import com.shipflow.notificationservice.domain.ai.AiLog; @@ -36,14 +37,15 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { String prompt = createDeadlinePrompt(command); - AiLog aiLog = aiLogRepository.save( - new AiLog( - command.relatedShipmentId(), - command.shipmentManagerId(), - prompt, - command.requestType() - ) + AiLog aiLog = new AiLog( + command.relatedShipmentId(), + command.shipmentManagerId(), + prompt, + command.requestType() ); + aiLog.markCreatedBy(command.ordererId()); + + aiLog = aiLogRepository.save(aiLog); try { AiResponseInfo result = aiGenerator.generate(prompt); @@ -64,15 +66,23 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { } } - public AiLogResult getAiLog(UUID aiId) { + //단건조회 + public AiLogResult getAiLog(UUID userId, String userRole, UUID aiId) { + validateMasterRole(userRole); AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); return AiLogResult.from(aiLog); } - public Page getAiLogs(Pageable pageable) { - return aiLogRepository.findAllByDeletedAtIsNull(pageable) + //목록조회 + public Page getAiLogs( + SearchAiLogCommand command, + Pageable pageable + ) { + validateMasterRole(command.userRole()); + + return aiLogRepository.search(command, pageable) .map(AiLogResult::from); } @@ -137,4 +147,10 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { workingHours ); } + + private void validateMasterRole(String userRole) { + if (!"MASTER".equals(userRole)) { + throw new BusinessException(AiErrorCode.FORBIDDEN_AI_ACCESS); + } + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java index 2128838..165ee6a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java @@ -8,16 +8,30 @@ import com.shipflow.notificationservice.domain.ai.type.AiRequestType; public record GenerateDeadlineCommand( + UUID orderId, + UUID ordererId, UUID relatedShipmentId, UUID shipmentManagerId, + String receiverSlackId, + + UUID supplierCompanyId, + UUID receiverCompanyId, + + UUID productId, + String product, + Integer quantity, + + UUID departureHubId, String fromHub, + UUID arrivalHubId, String toHub, List route, - String product, + String requestNote, LocalDateTime deadline, String workingHours, + AiRequestType requestType, LocalDate workDate ) { -} +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java new file mode 100644 index 0000000..8fe501d --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.application.ai.dto.command; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; + +public record SearchAiLogCommand( + UUID userId, + String userRole, + UUID shipmentManagerId, + AiRequestType requestType, + AiRequestStatus requestStatus, + LocalDate workDate, + LocalDateTime createdAtFrom, + LocalDateTime createdAtTo +) { +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 6e49c8c..008ee96 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -1,5 +1,6 @@ package com.shipflow.notificationservice.application.slack; +import java.util.Set; import java.util.UUID; import org.springframework.data.domain.Page; @@ -8,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional; import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -16,7 +18,6 @@ import com.shipflow.notificationservice.domain.slack.exception.SlackErrorCode; import com.shipflow.notificationservice.domain.slack.repository.SlackMessageRepository; import com.shipflow.notificationservice.domain.slack.vo.SlackSendInfo; -import com.shipflow.notificationservice.presentation.common.BasePageRequest; import lombok.RequiredArgsConstructor; @@ -29,19 +30,22 @@ public class SlackAppService { private final SlackSender slackSender; // 메시지 전송 - // TODO: 인증 적용 후 userId 받아 createdBy 처리 + // TODO: 자동 발송의 경우 receiverSlackId를 사용자/이벤트 정보 기반으로 조회하도록 분리 @Transactional public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { - - SlackMessage slackMessage = slackMessageRepository.save( - new SlackMessage( - command.receiverSlackId(), - command.relatedShipmentId(), - command.relatedAiLogId(), - command.message(), - command.messageType() - ) + //권한 확인 + validateCreateRole(command.userRole()); + + SlackMessage slackMessage = new SlackMessage( + command.receiverSlackId(), + command.relatedShipmentId(), + command.relatedAiLogId(), + command.message(), + command.messageType() ); + slackMessage.markCreatedBy(command.userId()); + + slackMessage = slackMessageRepository.save(slackMessage); try { SlackSendInfo result = slackSender.sendMessage( @@ -57,7 +61,9 @@ public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { } // 단건 조회 - public SlackMessageResult getSlackMessage(UUID slackId) { + public SlackMessageResult getSlackMessage(UUID userId, String userRole, UUID slackId) { + //권한 확인 + validateMasterRole(userRole); SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(slackId) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); @@ -65,18 +71,22 @@ public SlackMessageResult getSlackMessage(UUID slackId) { } // 목록 조회 - public Page getSlackMessages(BasePageRequest pageRequest) { - - Pageable pageable = pageRequest.toPageable(); - - return slackMessageRepository.findAllByDeletedAtIsNull(pageable) + public Page getSlackMessages( + SearchSlackMessageCommand command, + Pageable pageable + ) { + //권한 확인 + validateMasterRole(command.userRole()); + + return slackMessageRepository.search(command, pageable) .map(SlackMessageResult::from); } //슬랙 메세지 수정 - // TODO: 인증 적용 후 userId 받아 updatedBy 처리 @Transactional public SlackMessageResult updateSlackMessage(UpdateSlackMessageCommand command) { + //권한 확인 + validateMasterRole(command.userRole()); SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(command.slackId()) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); @@ -89,19 +99,20 @@ public SlackMessageResult updateSlackMessage(UpdateSlackMessageCommand command) command.message() ); - slackMessage.updateMessage(command.message()); + slackMessage.updateMessage(command.message(), command.userId()); return SlackMessageResult.from(slackMessage); } //슬랙 메세지 삭제 - // TODO: 인증 적용 후 userId 받아 deletedBy 처리 @Transactional - public void deleteSlackMessage(UUID slackId, UUID userId) { + public void deleteSlackMessage(UUID userId, String userRole, UUID slackId) { + //권한 확인 + validateMasterRole(userRole); + SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(slackId) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); - // 도메인 검증을 먼저 수행한 후, 외부 Slack 삭제 API 호출 slackMessage.validateDeletable(); slackSender.deleteMessage( @@ -111,4 +122,16 @@ public void deleteSlackMessage(UUID slackId, UUID userId) { slackMessage.markDeleted(userId); } + + private void validateCreateRole(String userRole) { + if (!Set.of("MASTER", "HUB_MANAGER", "DELIVERY_MANAGER", "COMPANY_MANAGER").contains(userRole)) { + throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); + } + } + + private void validateMasterRole(String userRole) { + if (!"MASTER".equals(userRole)) { + throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); + } + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java new file mode 100644 index 0000000..42d9260 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java @@ -0,0 +1,18 @@ +package com.shipflow.notificationservice.application.slack.dto.command; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; + +public record SearchSlackMessageCommand( + UUID userId, + String userRole, + String receiverSlackId, + SlackSendStatus sendStatus, + SlackMessageType messageType, + LocalDateTime createdAtFrom, + LocalDateTime createdAtTo +) { +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java index c349b9c..05259d5 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java @@ -5,6 +5,8 @@ import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; public record SendSlackMessageCommand( + UUID userId, + String userRole, String receiverSlackId, UUID relatedShipmentId, UUID relatedAiLogId, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java index 05aaa8a..6dda7fd 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java @@ -3,6 +3,8 @@ import java.util.UUID; public record UpdateSlackMessageCommand( + UUID userId, + String userRole, UUID slackId, String message ) { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java index f0b5df8..a99701c 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java @@ -45,7 +45,7 @@ public class AiLog extends BaseEntity { @Column(name = "work_date") private LocalDate workDate; - + @Enumerated(EnumType.STRING) @Column(name = "send_status", length = 20, nullable = false) private SlackSendStatus sendStatus; @@ -75,6 +75,10 @@ public AiLog(UUID relatedShipmentId, this.sendStatus = SlackSendStatus.PENDING; } + public void markCreatedBy(UUID userId) { + super.markCreated(userId); + } + //AI 성공 public void markSuccess(String responseText, LocalDateTime finalDeadlineAt) { this.responseText = responseText; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java index 92ceb34..4ca196a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java @@ -7,6 +7,7 @@ public enum AiErrorCode implements ErrorCode { // 조회 + FORBIDDEN_AI_ACCESS("FORBIDDEN_AI_ACCESS", HttpStatus.FORBIDDEN, "AI 접근 권한이 없습니다."), AI_LOG_NOT_FOUND("AI_LOG_NOT_FOUND", HttpStatus.NOT_FOUND, "AI 로그를 찾을 수 없습니다."), // 이벤트 diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java index a33f564..a380bee 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.domain.ai.AiLog; public interface AiLogRepository { @@ -14,5 +15,5 @@ public interface AiLogRepository { Optional findByIdAndDeletedAtIsNull(UUID id); - Page findAllByDeletedAtIsNull(Pageable pageable); + Page search(SearchAiLogCommand command, Pageable pageable); } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java index 64e99d3..cf5c1b1 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java @@ -82,6 +82,19 @@ public void markSuccess(String slackTs, String slackChannelId) { this.sentAt = LocalDateTime.now(); } + public void markCreatedBy(UUID userId) { + super.markCreated(userId); + } + + public void updateMessage(String newMessage, UUID userId) { + if (newMessage == null || newMessage.isBlank()) { + throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_REQUIRED); + } + + this.message = newMessage; + super.markUpdated(userId); + } + public void markDeleted(UUID userId) { validateDeletable(); super.softDelete(userId); @@ -104,15 +117,7 @@ public void validateUpdatable() { throw new BusinessException(SlackErrorCode.SLACK_CHANNEL_ID_REQUIRED); } } - - public void updateMessage(String newMessage) { - if (newMessage == null || newMessage.isBlank()) { - throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_REQUIRED); - } - - this.message = newMessage; - } - + public void validateDeletable() { if (this.getDeletedAt() != null) { throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND); @@ -126,5 +131,5 @@ public void validateDeletable() { public void markFail() { this.sendStatus = SlackSendStatus.FAIL; } - + } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java index 713116a..75cdd60 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java @@ -6,6 +6,7 @@ public enum SlackErrorCode implements ErrorCode { //슬랙 발송 + FORBIDDEN_SLACK_ACCESS("FORBIDDEN_SLACK_ACCESS", HttpStatus.FORBIDDEN, "슬랙 메시지 접근 권한이 없습니다."), SLACK_MESSAGE_NOT_FOUND("SLACK_MESSAGE_NOT_FOUND", HttpStatus.NOT_FOUND, "슬랙 메시지를 찾을 수 없습니다."), SLACK_SEND_FAILED("SLACK_SEND_FAILED", HttpStatus.BAD_REQUEST, "슬랙 메시지 발송에 실패했습니다."), INVALID_SLACK_ID_FORMAT("INVALID_SLACK_ID_FORMAT", HttpStatus.BAD_REQUEST, "지원하지 않는 Slack ID 형식입니다."), diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java index 0e7a638..42b22af 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.SlackMessage; public interface SlackMessageRepository { @@ -14,5 +15,5 @@ public interface SlackMessageRepository { Optional findByIdAndDeletedAtIsNull(UUID slackId); - Page findAllByDeletedAtIsNull(Pageable pageable); + Page search(SearchSlackMessageCommand command, Pageable pageable); } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 3eb2445..34d6795 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -12,8 +12,6 @@ @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class ShipmentCreatedEvent { - - private UUID shipmentId; private UUID orderId; private UUID ordererId; private UUID supplierCompanyId; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java index fbd1260..fcfc365 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java @@ -3,8 +3,6 @@ import java.util.Optional; import java.util.UUID; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.shipflow.notificationservice.domain.ai.AiLog; @@ -12,7 +10,4 @@ public interface AiLogJpaRepository extends JpaRepository { Optional findByIdAndDeletedAtIsNull(UUID id); - - Page findAllByDeletedAtIsNull(Pageable pageable); - } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java index 57895b8..b8f7931 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java @@ -1,14 +1,25 @@ package com.shipflow.notificationservice.infrastructure.persistence.ai; +import static com.shipflow.notificationservice.domain.ai.QAiLog.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.domain.ai.AiLog; import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import lombok.RequiredArgsConstructor; @@ -17,6 +28,7 @@ public class AiLogRepositoryImpl implements AiLogRepository { private final AiLogJpaRepository aiLogJpaRepository; + private final JPAQueryFactory queryFactory; @Override public AiLog save(AiLog aiLog) { @@ -29,8 +41,56 @@ public Optional findByIdAndDeletedAtIsNull(UUID aiId) { } @Override - public Page findAllByDeletedAtIsNull(Pageable pageable) { - return aiLogJpaRepository.findAllByDeletedAtIsNull(pageable); + public Page search(SearchAiLogCommand command, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + builder.and(aiLog.deletedAt.isNull()); + builder.and(shipmentManagerIdEq(command.shipmentManagerId())); + builder.and(requestTypeEq(command.requestType())); + builder.and(requestStatusEq(command.requestStatus())); + builder.and(workDateEq(command.workDate())); + builder.and(createdAtGoe(command.createdAtFrom())); + builder.and(createdAtLoe(command.createdAtTo())); + + List content = queryFactory + .selectFrom(aiLog) + .where(builder) + .orderBy(aiLog.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(aiLog.id.count()) + .from(aiLog) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); } + + private com.querydsl.core.types.Predicate shipmentManagerIdEq(UUID shipmentManagerId) { + return shipmentManagerId == null ? null : aiLog.shipmentManagerId.eq(shipmentManagerId); + } + + private com.querydsl.core.types.Predicate requestTypeEq(AiRequestType requestType) { + return requestType == null ? null : aiLog.requestType.eq(requestType); + } + + private com.querydsl.core.types.Predicate requestStatusEq(AiRequestStatus requestStatus) { + return requestStatus == null ? null : aiLog.requestStatus.eq(requestStatus); + } + + private com.querydsl.core.types.Predicate workDateEq(LocalDate workDate) { + return workDate == null ? null : aiLog.workDate.eq(workDate); + } + + private com.querydsl.core.types.Predicate createdAtGoe(LocalDateTime from) { + return from == null ? null : aiLog.createdAt.goe(from); + } + + private com.querydsl.core.types.Predicate createdAtLoe(LocalDateTime to) { + return to == null ? null : aiLog.createdAt.loe(to); + } + } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java index ae56d4d..009d6e6 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java @@ -3,17 +3,12 @@ import java.util.Optional; import java.util.UUID; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.shipflow.notificationservice.domain.slack.SlackMessage; public interface SlackMessageJpaRepository extends JpaRepository { - // TODO: 단건 조회 시 soft delete 제외 Optional findByIdAndDeletedAtIsNull(UUID id); - // TODO: 목록 조회 페이징 및 검색 처리 필요 - Page findAllByDeletedAtIsNull(Pageable pageable); } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java index 4639c1d..e0c4548 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java @@ -1,14 +1,24 @@ package com.shipflow.notificationservice.infrastructure.persistence.slack; +import static com.shipflow.notificationservice.domain.slack.QSlackMessage.*; + +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.SlackMessage; import com.shipflow.notificationservice.domain.slack.repository.SlackMessageRepository; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; import lombok.RequiredArgsConstructor; @@ -17,6 +27,7 @@ public class SlackMessageRepositoryImpl implements SlackMessageRepository { private final SlackMessageJpaRepository slackMessageJpaRepository; + private final JPAQueryFactory queryFactory; @Override public SlackMessage save(SlackMessage slackMessage) { @@ -29,7 +40,54 @@ public Optional findByIdAndDeletedAtIsNull(UUID slackId) { } @Override - public Page findAllByDeletedAtIsNull(Pageable pageable) { - return slackMessageJpaRepository.findAllByDeletedAtIsNull(pageable); + public Page search(SearchSlackMessageCommand command, Pageable pageable) { + + BooleanBuilder builder = new BooleanBuilder(); + builder.and(slackMessage.deletedAt.isNull()); + builder.and(receiverSlackIdEq(command.receiverSlackId())); + builder.and(sendStatusEq(command.sendStatus())); + builder.and(messageTypeEq(command.messageType())); + builder.and(createdAtGoe(command.createdAtFrom())); + builder.and(createdAtLoe(command.createdAtTo())); + + List content = queryFactory + .selectFrom(slackMessage) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(slackMessage.id.count()) + .from(slackMessage) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); } + + // ===== 조건 메서드 ===== + + private com.querydsl.core.types.Predicate receiverSlackIdEq(String receiverSlackId) { + return (receiverSlackId == null || receiverSlackId.isBlank()) + ? null + : slackMessage.receiverSlackId.eq(receiverSlackId); + } + + private com.querydsl.core.types.Predicate sendStatusEq(SlackSendStatus sendStatus) { + return sendStatus == null ? null : slackMessage.sendStatus.eq(sendStatus); + } + + private com.querydsl.core.types.Predicate messageTypeEq(SlackMessageType messageType) { + return messageType == null ? null : slackMessage.messageType.eq(messageType); + } + + private com.querydsl.core.types.Predicate createdAtGoe(LocalDateTime from) { + return from == null ? null : slackMessage.createdAt.goe(from); + } + + private com.querydsl.core.types.Predicate createdAtLoe(LocalDateTime to) { + return to == null ? null : slackMessage.createdAt.loe(to); + } + } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java index 708203e..4af8902 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -46,8 +46,9 @@ public record GenerateDeadlineRequest( LocalDate workDate ) { - public GenerateDeadlineCommand toCommand() { + public GenerateDeadlineCommand toCommand(UUID userId) { return new GenerateDeadlineCommand( + userId, relatedShipmentId, shipmentManagerId, fromHub, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index 9faccbf..e6a4ec1 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -1,19 +1,30 @@ package com.shipflow.notificationservice.presentation.ai.external; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.UUID; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Page; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; +import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; +import com.shipflow.notificationservice.presentation.common.BasePageRequest; import com.shipflow.notificationservice.presentation.common.BasePageResponse; import jakarta.validation.Valid; @@ -31,31 +42,69 @@ public AiController(AiAppService aiAppService) { // TODO: 이벤트 기반 전환 후 admin/debug 용으로 유지 @PostMapping public ApiResponse generateAiLog( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, @Valid @RequestBody GenerateDeadlineRequest request ) { + + if (!"MASTER".equals(userRole)) { + throw new BusinessException(AiErrorCode.FORBIDDEN_AI_ACCESS); + } + return ApiResponse.ok( AiLogResponse.from( - aiAppService.generateAiLog(request.toCommand()) + aiAppService.generateAiLog(request.toCommand(UUID.fromString(userId))) ) ); } @GetMapping("/{aiId}") - public ApiResponse getAiLog(@PathVariable UUID aiId) { + public ApiResponse getAiLog( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID aiId + ) { return ApiResponse.ok( AiLogResponse.from( - aiAppService.getAiLog(aiId) + aiAppService.getAiLog( + UUID.fromString(userId), + userRole, + aiId + ) ) ); } @GetMapping - public ApiResponse> getAiLogs(Pageable pageable) { - return ApiResponse.ok( - BasePageResponse.from( - aiAppService.getAiLogs(pageable) - .map(AiLogResponse::from) + public ApiResponse> getAiLogs( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @RequestParam(required = false) UUID shipmentManagerId, + @RequestParam(required = false) AiRequestType requestType, + @RequestParam(required = false) AiRequestStatus requestStatus, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate workDate, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtFrom, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtTo, + BasePageRequest pageRequest + ) { + Page page = aiAppService.getAiLogs( + new SearchAiLogCommand( + UUID.fromString(userId), + userRole, + shipmentManagerId, + requestType, + requestStatus, + workDate, + createdAtFrom, + createdAtTo + ), + pageRequest.toPageable() ) - ); + .map(AiLogResponse::from); + + return ApiResponse.ok(BasePageResponse.from(page)); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java index 271e945..19ac97d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java @@ -5,13 +5,13 @@ import org.springframework.data.domain.Sort; public record BasePageRequest( - int page, - int size + Integer page, + Integer size ) { public BasePageRequest { - page = Math.max(page, 0); + page = (page == null || page < 0) ? 0 : page; - if (size != 10 && size != 30 && size != 50) { + if (size == null || (size != 10 && size != 30 && size != 50)) { size = 10; } } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java index 85a5173..442fd5b 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java @@ -21,12 +21,14 @@ public record SendSlackMessageRequest( @NotBlank @Size(max = 1000) String message, - + @NotNull SlackMessageType messageType ) { - public SendSlackMessageCommand toCommand() { + public SendSlackMessageCommand toCommand(UUID userId, String userRole) { return new SendSlackMessageCommand( + userId, + userRole, receiverSlackId, relatedShipmentId, relatedAiLogId, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java index c6c2a2f..a254220 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java @@ -12,7 +12,11 @@ public record UpdateSlackMessageRequest( @Size(max = 1000) String message ) { - public UpdateSlackMessageCommand toCommand(UUID slackId) { - return new UpdateSlackMessageCommand(slackId, message); + public UpdateSlackMessageCommand toCommand(UUID userId, String userRole, UUID slackId) { + return new UpdateSlackMessageCommand( + userId, + userRole, + slackId, + message); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java index d6315ec..552b617 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java @@ -1,20 +1,27 @@ package com.shipflow.notificationservice.presentation.slack.external; +import java.time.LocalDateTime; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; import com.shipflow.notificationservice.application.slack.SlackAppService; -import com.shipflow.notificationservice.presentation.common.BasePageRequest; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; import com.shipflow.notificationservice.presentation.common.BasePageResponse; import com.shipflow.notificationservice.presentation.slack.dto.request.SendSlackMessageRequest; import com.shipflow.notificationservice.presentation.slack.dto.request.UpdateSlackMessageRequest; @@ -35,49 +42,92 @@ public SlackController(SlackAppService slackAppService) { } @PostMapping - public ApiResponse sendSlackMessage(@Valid @RequestBody SendSlackMessageRequest request) { + public ApiResponse sendSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @Valid @RequestBody SendSlackMessageRequest request) { return ApiResponse.ok( SlackMessageResponse.from( - slackAppService.sendSlackMessage(request.toCommand()) + slackAppService.sendSlackMessage( + request.toCommand(UUID.fromString(userId), userRole) + ) ) ); } @GetMapping("/{slackId}") - public ApiResponse getSlackMessage(@PathVariable UUID slackId) { - return ApiResponse.ok(SlackMessageResponse.from(slackAppService.getSlackMessage(slackId))); + public ApiResponse getSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID slackId) { + return ApiResponse.ok( + SlackMessageResponse.from( + slackAppService.getSlackMessage( + UUID.fromString(userId), + userRole, + slackId + ) + ) + ); } - // TODO: 목록 조회 검색 처리 필요 @GetMapping - public ApiResponse> getAllSlackMessages( - @ModelAttribute BasePageRequest pageRequest + public ApiResponse> getSlackMessages( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @RequestParam(required = false) String receiverSlackId, + @RequestParam(required = false) SlackSendStatus sendStatus, + @RequestParam(required = false) SlackMessageType messageType, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtFrom, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtTo, + Pageable pageable ) { - return ApiResponse.ok( - BasePageResponse.from( - slackAppService.getSlackMessages(pageRequest) - .map(SlackMessageResponse::from) + Page page = slackAppService.getSlackMessages( + new SearchSlackMessageCommand( + UUID.fromString(userId), + userRole, + receiverSlackId, + sendStatus, + messageType, + createdAtFrom, + createdAtTo + ), + pageable ) - ); + .map(SlackMessageResponse::from); + + return ApiResponse.ok(BasePageResponse.from(page)); } @PatchMapping("/{slackId}") public ApiResponse updateSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, @PathVariable UUID slackId, @Valid @RequestBody UpdateSlackMessageRequest request ) { return ApiResponse.ok( SlackMessageResponse.from( - slackAppService.updateSlackMessage(request.toCommand(slackId)) + slackAppService.updateSlackMessage( + request.toCommand(UUID.fromString(userId), userRole, slackId) + ) ) ); } @DeleteMapping("/{slackId}") - public ApiResponse deleteSlackMessage(@PathVariable UUID slackId) { - UUID userId = UUID.fromString( - "11111111-1111-1111-1111-111111111111"); // TODO: 인증 적용 후 실제 사용자 ID로 교체 (임시 system user) // TODO: Security 적용 후 교체 - slackAppService.deleteSlackMessage(slackId, userId); + public ApiResponse deleteSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID slackId + ) { + slackAppService.deleteSlackMessage( + UUID.fromString(userId), + userRole, + slackId + ); return ApiResponse.ok(null); } } \ No newline at end of file diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java index 0a6dc64..6faeb04 100644 --- a/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java +++ b/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java @@ -15,9 +15,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -51,10 +56,14 @@ class SendSlackMessageTest { @DisplayName("수동 발송에 성공하면 SUCCESS 상태로 저장") void sendSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID relatedShipmentId = UUID.randomUUID(); UUID relatedAiLogId = UUID.randomUUID(); SendSlackMessageCommand command = new SendSlackMessageCommand( + userId, + userRole, "U0APZGV2NRH", relatedShipmentId, relatedAiLogId, @@ -98,10 +107,14 @@ void sendSlackMessage_success() { @DisplayName("슬랙 전송에 실패하면 FAIL 상태로 저장") void sendSlackMessage_fail() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID relatedShipmentId = UUID.randomUUID(); UUID relatedAiLogId = UUID.randomUUID(); SendSlackMessageCommand command = new SendSlackMessageCommand( + userId, + userRole, "U0APZGV2NRH", relatedShipmentId, relatedAiLogId, @@ -138,6 +151,8 @@ class GetSlackMessageTest { @DisplayName("존재하는 메시지 단건 조회 성공") void getSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); SlackMessage slackMessage = new SlackMessage( @@ -153,7 +168,7 @@ void getSlackMessage_success() { .thenReturn(Optional.of(slackMessage)); // when - SlackMessageResult result = slackAppService.getSlackMessage(slackId); + SlackMessageResult result = slackAppService.getSlackMessage(userId, userRole, slackId); // then assertThat(result.receiverSlackId()).isEqualTo("U0APZGV2NRH"); @@ -165,13 +180,15 @@ void getSlackMessage_success() { @DisplayName("존재하지 않는 메시지 조회 시 BusinessException 발생") void getSlackMessage_notFound() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); when(slackMessageRepository.findByIdAndDeletedAtIsNull(slackId)) .thenReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> slackAppService.getSlackMessage(slackId)) + assertThatThrownBy(() -> slackAppService.getSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); } } @@ -184,6 +201,9 @@ class GetSlackMessagesTest { @DisplayName("삭제되지 않은 메시지 목록 전체 조회") void getSlackMessages_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; + SlackMessage first = new SlackMessage( "U0APZGV2NRH", UUID.randomUUID(), @@ -202,18 +222,23 @@ void getSlackMessages_success() { ); second.markFail(); - when(slackMessageRepository.findAllByDeletedAtIsNull()) - .thenReturn(List.of(first, second)); + Pageable pageable = PageRequest.of(0, 10); + SearchSlackMessageCommand command = new SearchSlackMessageCommand( + userId, userRole, null, null, null, null, null + ); + + when(slackMessageRepository.search(any(SearchSlackMessageCommand.class), any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of(first, second), pageable, 2)); // when - List results = slackAppService.getSlackMessages(); + Page results = slackAppService.getSlackMessages(command, pageable); // then - assertThat(results).hasSize(2); - assertThat(results.get(0).message()).isEqualTo("첫 번째 메시지"); - assertThat(results.get(0).sendStatus()).isEqualTo(SlackSendStatus.SUCCESS); - assertThat(results.get(1).message()).isEqualTo("두 번째 메시지"); - assertThat(results.get(1).sendStatus()).isEqualTo(SlackSendStatus.FAIL); + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).message()).isEqualTo("첫 번째 메시지"); + assertThat(results.getContent().get(0).sendStatus()).isEqualTo(SlackSendStatus.SUCCESS); + assertThat(results.getContent().get(1).message()).isEqualTo("두 번째 메시지"); + assertThat(results.getContent().get(1).sendStatus()).isEqualTo(SlackSendStatus.FAIL); } } @@ -225,9 +250,13 @@ class UpdateSlackMessageTest { @DisplayName("SUCCESS 상태의 메시지는 수정 가능") void updateSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -264,9 +293,13 @@ void updateSlackMessage_success() { @DisplayName("존재하지 않는 메시지 수정 시 BusinessException 발생") void updateSlackMessage_notFound() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -285,9 +318,13 @@ void updateSlackMessage_notFound() { @DisplayName("아직 발송되지 않은(slackTs 없는) 메시지는 수정 불가") void updateSlackMessage_notSent() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -322,6 +359,7 @@ void deleteSlackMessage_success() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; SlackMessage slackMessage = new SlackMessage( "U0APZGV2NRH", @@ -342,7 +380,7 @@ void deleteSlackMessage_success() { )); // when - slackAppService.deleteSlackMessage(slackId, userId); + slackAppService.deleteSlackMessage(userId, userRole, slackId); // then verify(slackSender).deleteMessage("C0AQ2G43EUD", "1742891400.123456"); @@ -356,12 +394,13 @@ void deleteSlackMessage_notFound() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; when(slackMessageRepository.findByIdAndDeletedAtIsNull(slackId)) .thenReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> slackAppService.deleteSlackMessage(slackId, userId)) + assertThatThrownBy(() -> slackAppService.deleteSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); verify(slackSender, never()).deleteMessage(any(), any()); @@ -373,6 +412,7 @@ void deleteSlackMessage_failStatus() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; SlackMessage slackMessage = new SlackMessage( "U0APZGV2NRH", @@ -391,7 +431,7 @@ void deleteSlackMessage_failStatus() { // null channelId/ts로 deleteMessage가 호출될 수 있음 → 실제 동작 확인 후 조정 // when & then - assertThatThrownBy(() -> slackAppService.deleteSlackMessage(slackId, userId)) + assertThatThrownBy(() -> slackAppService.deleteSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); verify(slackSender, never()).deleteMessage(any(), any()); From b6f66f8f4f2a6385f0e1303dff26a4045de57d75 Mon Sep 17 00:00:00 2001 From: 250 Date: Mon, 6 Apr 2026 22:13:40 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat(notification):=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=B0=98=20=EB=B0=B0=EC=86=A1=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notification-service/build.gradle | 1 + .../NotificationOrchestratorService.java | 100 ++++++--- .../application/ai/AiAppService.java | 18 +- .../client/order/OrderInternalClient.java | 14 ++ .../client/order/OrderReadModelResponse.java | 12 ++ .../consumer/ShipmentCreatedConsumer.java | 31 --- .../consumer/ShipmentCreatedHandler.java | 21 ++ .../consumer/ShipmentCreatedListener.java | 21 ++ .../messaging/dto/ShipmentCreatedEvent.java | 34 ++- .../dto/request/GenerateDeadlineRequest.java | 63 +++--- .../ai/external/AiController.java | 4 +- .../notificationservice/AiAppServiceTest.java | 4 +- .../NotificationOrchestratorServiceTest.java | 202 ++++++++++++++++++ 13 files changed, 409 insertions(+), 116 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java delete mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java create mode 100644 notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java diff --git a/notification-service/build.gradle b/notification-service/build.gradle index 7f6e30f..e1f7f60 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -28,6 +28,7 @@ dependencies { // 1. Web / External API implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' // 2. Persistence (DB) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java index 5737c1c..52389cf 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java @@ -1,6 +1,7 @@ package com.shipflow.notificationservice.application; import java.util.Collections; +import java.util.List; import java.util.UUID; import org.springframework.stereotype.Service; @@ -17,6 +18,8 @@ import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.infrastructure.client.order.OrderInternalClient; +import com.shipflow.notificationservice.infrastructure.client.order.OrderReadModelResponse; import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; import lombok.RequiredArgsConstructor; @@ -28,60 +31,65 @@ public class NotificationOrchestratorService { private static final String DEFAULT_WORKING_HOURS = "09:00 ~ 18:00"; private static final String UNKNOWN = "확인 필요"; + private static final UUID SYSTEM_USER_ID = new UUID(0L, 0L); private final AiAppService aiAppService; private final SlackAppService slackAppService; private final AiLogRepository aiLogRepository; + private final OrderInternalClient orderInternalClient; @Transactional public void handleShipmentCreated(ShipmentCreatedEvent event) { - GenerateDeadlineCommand command = toGenerateDeadlineCommand(event); + OrderReadModelResponse order = getOrderReadModel(event.getOrderId()); + String slackId = resolveSlackId(event); + GenerateDeadlineCommand command = toGenerateDeadlineCommand(event, order, slackId); AiLogResult aiResult = aiAppService.generateAiLog(command); - String slackMessage = createDeadlineSlackMessage(command, aiResult); try { slackAppService.sendSlackMessage( new SendSlackMessageCommand( - event.getOrdererId(), - "MASTER", // TODO: 내부 시스템 자동 발송 권한 처리 방식 확정 후 변경 - event.getReceiverSlackId(), - command.relatedShipmentId(), + SYSTEM_USER_ID, + "MASTER", + slackId, + event.getShipmentId(), aiResult.aiId(), slackMessage, SlackMessageType.DEADLINE_ALERT ) ); - markSlackSendSuccess(aiResult.aiId()); - } catch (Exception e) { markSlackSendFail(aiResult.aiId()); throw e; } } - private GenerateDeadlineCommand toGenerateDeadlineCommand(ShipmentCreatedEvent event) { + private GenerateDeadlineCommand toGenerateDeadlineCommand( + ShipmentCreatedEvent event, + OrderReadModelResponse order, + String slackId + ) { return new GenerateDeadlineCommand( event.getOrderId(), - event.getOrdererId(), - null, // relatedShipmentId: 현재 이벤트에 없으면 추후 보강 - null, // shipmentManagerId: 현재 이벤트에 없으면 추후 보강 - event.getReceiverSlackId(), + SYSTEM_USER_ID, + event.getShipmentId(), + null, + slackId, - event.getSupplierCompanyId(), - event.getReceiverCompanyId(), + null, + null, event.getProductId(), - extractProductText(event), + extractProductText(order, event), event.getQuantity(), event.getDepartureHubId(), - extractFromHub(event), + order != null ? order.departureHubName() : UNKNOWN, event.getArrivalHubId(), - extractToHub(event), - Collections.emptyList(), // route: 허브 내부 API 연동 전까지 기본값 + order != null ? order.arrivalHubName() : UNKNOWN, + extractRouteTexts(event), extractRequestNote(event), event.getRequestDeadline(), @@ -92,23 +100,46 @@ private GenerateDeadlineCommand toGenerateDeadlineCommand(ShipmentCreatedEvent e ); } - private String extractFromHub(ShipmentCreatedEvent event) { - return event.getDepartureHubId() == null - ? UNKNOWN - : "허브ID: " + event.getDepartureHubId(); + private OrderReadModelResponse getOrderReadModel(UUID orderId) { + if (orderId == null) + return null; + try { + return orderInternalClient.getOrderReadModel(orderId); + } catch (Exception e) { + return null; + } } - private String extractToHub(ShipmentCreatedEvent event) { - return event.getArrivalHubId() == null + private String resolveSlackId(ShipmentCreatedEvent event) { + String slackId = event.getShipmentManagerSlackId(); + return (slackId == null || slackId.isBlank()) ? UNKNOWN : slackId; + } + + private String extractProductText(OrderReadModelResponse order, ShipmentCreatedEvent event) { + String productName = (order == null || order.productName() == null || order.productName().isBlank()) ? UNKNOWN - : "허브ID: " + event.getArrivalHubId(); + : order.productName(); + + String quantity = event.getQuantity() == null + ? "수량 미확인" + : event.getQuantity() + "개"; + + return productName + " / 수량: " + quantity; } - private String extractProductText(ShipmentCreatedEvent event) { - String productId = event.getProductId() == null ? UNKNOWN : event.getProductId().toString(); - String quantity = event.getQuantity() == null ? "수량 미확인" : event.getQuantity() + "개"; + private List extractRouteTexts(ShipmentCreatedEvent event) { + if (event.getRoutes() == null || event.getRoutes().isEmpty()) { + return Collections.emptyList(); + } - return "상품ID: " + productId + " / 수량: " + quantity; + return event.getRoutes().stream() + .map(route -> { + if (route.getSequence() == null) { + return "경유"; + } + return route.getSequence() + "번 경유"; + }) + .toList(); } private String extractRequestNote(ShipmentCreatedEvent event) { @@ -120,7 +151,7 @@ private String extractRequestNote(ShipmentCreatedEvent event) { private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLogResult aiResult) { String routeText = (command.route() == null || command.route().isEmpty()) ? "없음" - : String.join(" → ", command.route()); + : String.join("\n", command.route()); String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) ? "없음" @@ -130,11 +161,13 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog 🚚 배송 요청 알림 주문 번호: %s + 배송 번호: %s 상품 정보: %s 요청 사항: %s 발송지: %s - 경유지: %s + 경유 경로: + %s 도착지: %s ⏰ AI 계산 최종 발송 시한: %s @@ -142,6 +175,7 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog ※ 해당 시간 이전에 발송을 완료해주세요. """.formatted( command.orderId(), + command.relatedShipmentId(), command.product(), requestNote, command.fromHub(), @@ -154,14 +188,12 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog private void markSlackSendSuccess(UUID aiLogId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); - aiLog.markSendSuccess(); } private void markSlackSendFail(UUID aiLogId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); - aiLog.markSendFail(); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index 119608a..9afa684 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -30,7 +30,6 @@ public class AiAppService { private final AiLogRepository aiLogRepository; private final AiGenerator aiGenerator; - //테스트용 외부(AI만 실행) @Transactional public AiLogResult generateAiLog(GenerateDeadlineCommand command) { validateCommand(command); @@ -66,16 +65,15 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { } } - //단건조회 public AiLogResult getAiLog(UUID userId, String userRole, UUID aiId) { validateMasterRole(userRole); + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); return AiLogResult.from(aiLog); } - //목록조회 public Page getAiLogs( SearchAiLogCommand command, Pageable pageable @@ -90,6 +88,12 @@ private void validateCommand(GenerateDeadlineCommand command) { if (command == null) { throw new BusinessException(AiErrorCode.AI_EVENT_NOT_FOUND); } + if (command.orderId() == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } + if (command.ordererId() == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } if (command.requestType() == null) { throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); } @@ -105,12 +109,14 @@ private void validateCommand(GenerateDeadlineCommand command) { if (command.product() == null || command.product().isBlank()) { throw new BusinessException(AiErrorCode.AI_PRODUCT_REQUIRED); } + if (command.quantity() == null || command.quantity() <= 0) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } if (command.deadline() == null) { throw new BusinessException(AiErrorCode.AI_DEADLINE_REQUIRED); } } - // AI 요청용 프롬프트 (AI 입력) private String createDeadlinePrompt(GenerateDeadlineCommand command) { String routeText = (command.route() == null || command.route().isEmpty()) ? "없음" @@ -127,10 +133,12 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { return """ 다음 물류 정보를 바탕으로 최종 발송 시한을 계산해라. + 주문 번호: %s 발송지: %s 경유지: %s 도착지: %s 상품: %s + 수량: %d 요청사항: %s 납기: %s 근무시간: %s @@ -138,10 +146,12 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { 반드시 ISO-8601 형식의 발송 시한만 포함해서 응답해라. 예시: 2026-04-04T09:00:00 """.formatted( + command.orderId(), command.fromHub(), routeText, command.toHub(), command.product(), + command.quantity(), requestNote, command.deadline(), workingHours diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java new file mode 100644 index 0000000..b851826 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java @@ -0,0 +1,14 @@ +package com.shipflow.notificationservice.infrastructure.client.order; + +import java.util.UUID; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "order-service") +public interface OrderInternalClient { + + @GetMapping("/internal/orders/{orderId}/read-model") + OrderReadModelResponse getOrderReadModel(@PathVariable UUID orderId); +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java new file mode 100644 index 0000000..280bd1f --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java @@ -0,0 +1,12 @@ +package com.shipflow.notificationservice.infrastructure.client.order; + +import java.util.UUID; + +public record OrderReadModelResponse( + UUID orderId, + String productId, + String productName, + String departureHubName, + String arrivalHubName +) { +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java deleted file mode 100644 index 884600c..0000000 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.shipflow.notificationservice.infrastructure.messaging.consumer; - -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.stereotype.Component; - -import com.shipflow.notificationservice.application.NotificationOrchestratorService; -import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; -import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -@RequiredArgsConstructor -public class ShipmentCreatedConsumer { - - private final NotificationOrchestratorService notificationOrchestratorService; - - @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) - public void handleShipmentCreated(ShipmentCreatedEvent event) { - log.info( - "[ShipmentCreatedConsumer] shipment.created 수신 - shipmentId={}, orderId={}, receiverSlackId={}", - event.getShipmentId(), - event.getOrderId(), - event.getReceiverSlackId() - ); - - notificationOrchestratorService.handleShipmentCreated(event); - } -} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java new file mode 100644 index 0000000..cd4cb3d --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java @@ -0,0 +1,21 @@ +package com.shipflow.notificationservice.infrastructure.messaging.consumer; + +import org.springframework.stereotype.Component; + +import com.shipflow.common.messaging.handler.AbstractSagaHandler; +import com.shipflow.notificationservice.application.NotificationOrchestratorService; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ShipmentCreatedHandler extends AbstractSagaHandler { + + private final NotificationOrchestratorService notificationOrchestratorService; + + @Override + protected void process(ShipmentCreatedEvent event) { + notificationOrchestratorService.handleShipmentCreated(event); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java new file mode 100644 index 0000000..ce0761e --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java @@ -0,0 +1,21 @@ +package com.shipflow.notificationservice.infrastructure.messaging.consumer; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ShipmentCreatedListener { + + private final ShipmentCreatedHandler shipmentCreatedHandler; + + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) + public void onShipmentCreated(ShipmentCreatedEvent event) { + shipmentCreatedHandler.handle(event); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 34d6795..5b974d4 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -1,27 +1,49 @@ package com.shipflow.notificationservice.infrastructure.messaging.dto; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.shipflow.common.messaging.event.SagaEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@AllArgsConstructor +@Builder @JsonIgnoreProperties(ignoreUnknown = true) -public class ShipmentCreatedEvent { +public class ShipmentCreatedEvent extends SagaEvent { + private UUID orderId; - private UUID ordererId; - private UUID supplierCompanyId; - private UUID receiverCompanyId; - private String receiverSlackId; + private UUID shipmentId; + private UUID productId; private Integer quantity; + private UUID departureHubId; private UUID arrivalHubId; + private LocalDateTime requestDeadline; private String requestNote; - private LocalDateTime occurredAt; + + //Todo: 생성 확인 + private String shipmentManagerSlackId; + + private List routes; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public static class RouteInfo { + private Integer sequence; + private UUID departureHubId; + private UUID arrivalHubId; + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java index 4af8902..f7d3545 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -1,6 +1,5 @@ package com.shipflow.notificationservice.presentation.ai.dto.request; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -8,58 +7,46 @@ import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record GenerateDeadlineRequest( - - @NotNull(message = "relatedShipmentId는 필수입니다.") - UUID relatedShipmentId, - - @NotNull(message = "shipmentManagerId는 필수입니다.") - UUID shipmentManagerId, - - @NotBlank(message = "fromHub는 필수입니다.") - String fromHub, - - @NotBlank(message = "toHub는 필수입니다.") - String toHub, - + @NotNull UUID relatedShipmentId, + @NotNull UUID shipmentManagerId, + @NotNull String receiverSlackId, + @NotNull UUID productId, + @NotNull String product, + @NotNull Integer quantity, + @NotNull UUID departureHubId, + @NotNull String fromHub, + @NotNull UUID arrivalHubId, + @NotNull String toHub, List route, - - @NotBlank(message = "product는 필수입니다.") - String product, - String requestNote, - - @NotNull(message = "deadline은 필수입니다.") - LocalDateTime deadline, - - String workingHours, - - @NotNull(message = "requestType은 필수입니다.") - AiRequestType requestType, - - @NotBlank(message = "receiverSlackId는 필수입니다.") - String receiverSlackId, - - LocalDate workDate + @NotNull LocalDateTime deadline, + String workingHours ) { - - public GenerateDeadlineCommand toCommand(UUID userId) { + public GenerateDeadlineCommand toCommand(UUID ordererId) { return new GenerateDeadlineCommand( - userId, + null, + ordererId, relatedShipmentId, shipmentManagerId, + receiverSlackId, + null, + null, + productId, + product, + quantity, + departureHubId, fromHub, + arrivalHubId, toHub, route, - product, requestNote, deadline, workingHours, - requestType, - workDate + AiRequestType.DEADLINE, + null ); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index e6a4ec1..9bb07fd 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -39,8 +39,8 @@ public AiController(AiAppService aiAppService) { this.aiAppService = aiAppService; } - // TODO: 이벤트 기반 전환 후 admin/debug 용으로 유지 - @PostMapping + // debug 용으로 유지 + @PostMapping("/debug") public ApiResponse generateAiLog( @RequestHeader("X-User-Id") String userId, @RequestHeader("X-User-Role") String userRole, diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java index c69fb50..512a5c6 100644 --- a/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java +++ b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java @@ -153,6 +153,8 @@ void deadline_null() { @Test void get_success() { UUID id = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; AiLog aiLog = new AiLog( UUID.randomUUID(), @@ -166,7 +168,7 @@ void get_success() { when(aiLogRepository.findByIdAndDeletedAtIsNull(id)) .thenReturn(Optional.of(aiLog)); - AiLogResult result = aiAppService.getAiLog(id); + AiLogResult result = aiAppService.getAiLog(userId, userRole, id); // 3개로 수정 assertThat(result.requestStatus()).isEqualTo(AiRequestStatus.SUCCESS); } diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java new file mode 100644 index 0000000..cc95960 --- /dev/null +++ b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java @@ -0,0 +1,202 @@ +package com.shipflow.notificationservice; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.shipflow.notificationservice.application.NotificationOrchestratorService; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.infrastructure.client.order.OrderInternalClient; +import com.shipflow.notificationservice.infrastructure.client.order.OrderReadModelResponse; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +@ExtendWith(MockitoExtension.class) +class NotificationOrchestratorServiceTest { + + @Mock + private AiAppService aiAppService; + @Mock + private SlackAppService slackAppService; + @Mock + private AiLogRepository aiLogRepository; + @Mock + private OrderInternalClient orderInternalClient; + + @InjectMocks + private NotificationOrchestratorService notificationOrchestratorService; + + @Test + @DisplayName("정상적인 이벤트 수신 시 슬랙 알림 발송 성공") + void handleShipmentCreated_success() { + // given + ShipmentCreatedEvent event = createEvent("U123SLACK"); + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID().toString(), + "마른 오징어", + "경기 북부 센터", + "부산광역시 센터" + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(event.getOrderId())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + + verify(orderInternalClient).getOrderReadModel(event.getOrderId()); + verify(aiAppService).generateAiLog(any()); + verify(slackAppService).sendSlackMessage(any()); + } + + @Test + @DisplayName("order read-model 조회 실패 시 UNKNOWN으로 대체하여 진행") + void handleShipmentCreated_orderFail_continueWithUnknown() { + // given + ShipmentCreatedEvent event = createEvent("U123SLACK"); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenThrow(new RuntimeException("조회 실패")); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + } + + @Test + @DisplayName("slackId가 null이면 UNKNOWN으로 대체") + void handleShipmentCreated_nullSlackId() { + // given + ShipmentCreatedEvent event = createEvent(null); // slackId null + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID().toString(), + "마른 오징어", + "경기 북부 센터", + "부산광역시 센터" + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then - UNKNOWN으로 대체되어 진행됨 + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + } + + @Test + @DisplayName("경유지가 있으면 sequence 기반 경유 텍스트 생성") + void handleShipmentCreated_withRoutes() { + // given + ShipmentCreatedEvent event = createEventWithRoutes("U123SLACK"); + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID().toString(), + "마른 오징어", + "경기 북부 센터", + "부산광역시 센터" + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + + verify(aiAppService).generateAiLog(argThat(command -> + command.route().contains("1번 경유") && command.route().contains("2번 경유") + )); + } + + // ── 픽스처 ────────────────────────────────────── + + private ShipmentCreatedEvent createEvent(String slackId) { + // reflection으로 필드 세팅 (getter only라서) + ShipmentCreatedEvent event = new ShipmentCreatedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + 10, + UUID.randomUUID(), + UUID.randomUUID(), + LocalDateTime.now().plusDays(3), + "빨리 보내주세요", + slackId, + null + ); + return event; + } + + private ShipmentCreatedEvent createEventWithRoutes(String slackId) { + ShipmentCreatedEvent event = new ShipmentCreatedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + 10, + UUID.randomUUID(), + UUID.randomUUID(), + LocalDateTime.now().plusDays(3), + "빨리 보내주세요", + slackId, + List.of( + new ShipmentCreatedEvent.RouteInfo(1, UUID.randomUUID(), UUID.randomUUID()), + new ShipmentCreatedEvent.RouteInfo(2, UUID.randomUUID(), UUID.randomUUID()) + ) + ); + return event; + } +} \ No newline at end of file From 1d7235a093b4ffe084dd36a17e977e82ce29ba9a Mon Sep 17 00:00:00 2001 From: 250 Date: Mon, 6 Apr 2026 23:32:40 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat(notification):=20=EC=9C=A0=EB=A0=88?= =?UTF-8?q?=EC=B9=B4=20=EC=84=9C=EB=B2=84=20=ED=8F=AC=ED=8A=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationserviceApplication.java | 2 ++ .../src/main/resources/application.yaml | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java b/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java index 64e0338..1b9e7dc 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cloud.openfeign.EnableFeignClients; @ConfigurationPropertiesScan @SpringBootApplication +@EnableFeignClients public class NotificationserviceApplication { public static void main(String[] args) { diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index b09de44..29c165e 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -4,9 +4,9 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=notification - username: ${DB_USER} - password: ${DB_PASSWORD} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:shipflow}?currentSchema=notification + username: ${DB_USER:shipflow} + password: ${DB_PASSWORD:1234} jpa: hibernate: @@ -29,4 +29,16 @@ slack: gemini: api-key: ${GEMINI_API_KEY} - url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent \ No newline at end of file + url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + +server: + port: 8087 + +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ + instance: + prefer-ip-address: true \ No newline at end of file From ab9fa1208ae8f1f15923d6cde1f68af094937691 Mon Sep 17 00:00:00 2001 From: 250 Date: Tue, 7 Apr 2026 04:25:05 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat(notification):=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=B0=98=20AI/Slack=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20Gateway=20=EC=97=B0=EB=8F=99,=20Swagger=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipflow/common/domain/BaseEntity.java | 8 --- docker-compose.yml | 16 ++++-- notification-service/build.gradle | 21 +++++--- .../NotificationOrchestratorService.java | 52 +++++++++++++------ .../application/ai/AiAppService.java | 1 - .../application/slack/SlackAppService.java | 3 +- .../notificationservice/config/JPAConfig.java | 22 ++++++-- .../config/SwaggerConfig.java | 20 +++++++ .../notificationservice/domain/ai/AiLog.java | 4 -- .../domain/slack/SlackMessage.java | 10 +--- .../client/ai/GeminiApiClient.java | 13 +++-- .../client/ai/config/GeminiApiConfig.java | 4 +- .../client/order/OrderInternalClient.java | 2 +- .../client/order/OrderReadModelResponse.java | 10 +++- .../dto/request/GenerateDeadlineRequest.java | 3 +- .../slack/external/SlackController.java | 4 +- .../src/main/resources/application.yaml | 25 +++++---- .../NotificationOrchestratorServiceTest.java | 39 +++++++++----- 18 files changed, 167 insertions(+), 90 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index 8e1a554..6a218ca 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -41,14 +41,6 @@ public abstract class BaseEntity { protected LocalDateTime deletedAt; protected UUID deletedBy; - - protected void markCreated(UUID userId) { - this.createdBy = userId; - } - - protected void markUpdated(UUID userId) { - this.updatedBy = userId; - } protected void softDelete(UUID userId) { this.deletedAt = LocalDateTime.now(); diff --git a/docker-compose.yml b/docker-compose.yml index 984794f..648c753 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - shipflow_postgres_data:/var/lib/postgresql/data - ./docker/postgres/init:/docker-entrypoint-initdb.d healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ] interval: 10s timeout: 5s retries: 5 @@ -43,7 +43,7 @@ services: ports: - "6379:6379" healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -59,7 +59,7 @@ services: ports: - "8761:8761" healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8761/actuator/health"] + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8761/actuator/health" ] interval: 10s timeout: 5s retries: 5 @@ -113,9 +113,15 @@ services: args: MODULE: notification-service ports: - - "8080" + - "8087:8080" environment: <<: *service-env + SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} + GEMINI_API_KEY: ${GEMINI_API_KEY} + RABBITMQ_HOST: rabbitmq + RABBITMQ_PORT: 5672 + RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} + RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} depends_on: <<: *service-depends-on @@ -183,7 +189,7 @@ services: volumes: - shipflow_rabbitmq_data:/var/lib/rabbitmq healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] interval: 10s timeout: 5s retries: 10 diff --git a/notification-service/build.gradle b/notification-service/build.gradle index e1f7f60..f4de34f 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -30,33 +30,40 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - // 2. Persistence (DB) + // 2. Service Discovery + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + // 3. Persistence (DB) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'org.postgresql:postgresql' - // 3. External Services + // 4. External Services implementation 'com.slack.api:slack-api-client:1.45.3' - // 4. QueryDSL + // 5. QueryDSL implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' - // 5. Validation + // 6. Validation implementation 'org.springframework.boot:spring-boot-starter-validation' - // 6. Common Module + // 7. Common Module implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter-amqp' - // 7. Lombok + // 8. Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - // 8. Test + // 9. Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // 10. Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' } tasks.named('test') { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java index 52389cf..5c51d26 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java @@ -26,7 +26,6 @@ @RequiredArgsConstructor @Service -@Transactional(readOnly = true) public class NotificationOrchestratorService { private static final String DEFAULT_WORKING_HOURS = "09:00 ~ 18:00"; @@ -38,15 +37,17 @@ public class NotificationOrchestratorService { private final AiLogRepository aiLogRepository; private final OrderInternalClient orderInternalClient; - @Transactional public void handleShipmentCreated(ShipmentCreatedEvent event) { + //1. 주문 정보 조회 OrderReadModelResponse order = getOrderReadModel(event.getOrderId()); String slackId = resolveSlackId(event); + // 2. AI 호출 + AiLog 저장 → AiAppService GenerateDeadlineCommand command = toGenerateDeadlineCommand(event, order, slackId); AiLogResult aiResult = aiAppService.generateAiLog(command); - String slackMessage = createDeadlineSlackMessage(command, aiResult); - + // 3. 슬랙 메시지 생성 + String slackMessage = createDeadlineSlackMessage(command, aiResult, order); + // 4. 슬랙 발송 + SlackMessage 저장 → SlackAppService 내부 try { slackAppService.sendSlackMessage( new SendSlackMessageCommand( @@ -59,8 +60,10 @@ public void handleShipmentCreated(ShipmentCreatedEvent event) { SlackMessageType.DEADLINE_ALERT ) ); + // 5. 슬랙 발송 성공 → AiLog 상태 업데이트 (별도 트랜잭션) markSlackSendSuccess(aiResult.aiId()); } catch (Exception e) { + // 6. 슬랙 발송 실패 → AiLog 상태 업데이트 (별도 트랜잭션) markSlackSendFail(aiResult.aiId()); throw e; } @@ -82,7 +85,7 @@ private GenerateDeadlineCommand toGenerateDeadlineCommand( null, event.getProductId(), - extractProductText(order, event), + extractProductText(order), event.getQuantity(), event.getDepartureHubId(), @@ -115,16 +118,10 @@ private String resolveSlackId(ShipmentCreatedEvent event) { return (slackId == null || slackId.isBlank()) ? UNKNOWN : slackId; } - private String extractProductText(OrderReadModelResponse order, ShipmentCreatedEvent event) { - String productName = (order == null || order.productName() == null || order.productName().isBlank()) + private String extractProductText(OrderReadModelResponse order) { + return (order == null || order.productName() == null || order.productName().isBlank()) ? UNKNOWN : order.productName(); - - String quantity = event.getQuantity() == null - ? "수량 미확인" - : event.getQuantity() + "개"; - - return productName + " / 수량: " + quantity; } private List extractRouteTexts(ShipmentCreatedEvent event) { @@ -148,7 +145,11 @@ private String extractRequestNote(ShipmentCreatedEvent event) { : event.getRequestNote(); } - private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLogResult aiResult) { + private String createDeadlineSlackMessage( + GenerateDeadlineCommand command, + AiLogResult aiResult, + OrderReadModelResponse order + ) { String routeText = (command.route() == null || command.route().isEmpty()) ? "없음" : String.join("\n", command.route()); @@ -157,13 +158,24 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog ? "없음" : command.requestNote(); + String ordererName = (order == null || order.ordererName() == null) + ? UNKNOWN + : order.ordererName(); + + String orderTime = (order == null || order.createdAt() == null) + ? UNKNOWN + : order.createdAt().toString(); + return """ 🚚 배송 요청 알림 주문 번호: %s + 주문자 정보: %s + 주문 시간: %s 배송 번호: %s - 상품 정보: %s + 상품 정보: %s / 수량: %d개 요청 사항: %s + 납기 기한: %s 발송지: %s 경유 경로: @@ -175,9 +187,13 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog ※ 해당 시간 이전에 발송을 완료해주세요. """.formatted( command.orderId(), + ordererName, + orderTime, command.relatedShipmentId(), command.product(), + command.quantity(), // 수량 requestNote, + command.deadline(), // 납기 기한 command.fromHub(), routeText, command.toHub(), @@ -185,13 +201,15 @@ private String createDeadlineSlackMessage(GenerateDeadlineCommand command, AiLog ); } - private void markSlackSendSuccess(UUID aiLogId) { + @Transactional + public void markSlackSendSuccess(UUID aiLogId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); aiLog.markSendSuccess(); } - private void markSlackSendFail(UUID aiLogId) { + @Transactional + public void markSlackSendFail(UUID aiLogId) { AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); aiLog.markSendFail(); diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index 9afa684..e91301e 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -42,7 +42,6 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { prompt, command.requestType() ); - aiLog.markCreatedBy(command.ordererId()); aiLog = aiLogRepository.save(aiLog); diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 008ee96..762ed53 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -43,7 +43,6 @@ public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { command.message(), command.messageType() ); - slackMessage.markCreatedBy(command.userId()); slackMessage = slackMessageRepository.save(slackMessage); @@ -99,7 +98,7 @@ public SlackMessageResult updateSlackMessage(UpdateSlackMessageCommand command) command.message() ); - slackMessage.updateMessage(command.message(), command.userId()); + slackMessage.updateMessage(command.message()); return SlackMessageResult.from(slackMessage); } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java index eb51ec0..71509ef 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java @@ -11,6 +11,8 @@ import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -24,6 +26,8 @@ @EntityScan(basePackages = "com.shipflow.notificationservice") public class JPAConfig { + private static final UUID SYSTEM_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + @PersistenceContext private EntityManager em; @@ -36,11 +40,19 @@ public JPAQueryFactory jpaQueryFactory() { @Bean public AuditorAware auditorAware() { return () -> { - /* - * TODO: Spring Security 연동 시 아래 방식으로 변경 - * - SecurityContext에서 로그인 사용자 UUID 추출하여 반환 - */ - return Optional.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); + try { + ServletRequestAttributes attrs = + (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); + // RabbitMQ 컨슈머 스레드 등 요청 컨텍스트가 없는 경우 → 시스템 UUID + if (attrs == null) + return Optional.of(SYSTEM_UUID); + String userId = attrs.getRequest().getHeader("X-User-Id"); + if (userId == null || userId.isBlank()) + return Optional.of(SYSTEM_UUID); + return Optional.of(UUID.fromString(userId)); + } catch (Exception e) { + return Optional.of(SYSTEM_UUID); + } }; } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java new file mode 100644 index 0000000..7994772 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Notification Service API") + .description("슬랙 메시지 및 AI 발송 시한 관리 API") + .version("v1")); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java index a99701c..1214319 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java @@ -75,10 +75,6 @@ public AiLog(UUID relatedShipmentId, this.sendStatus = SlackSendStatus.PENDING; } - public void markCreatedBy(UUID userId) { - super.markCreated(userId); - } - //AI 성공 public void markSuccess(String responseText, LocalDateTime finalDeadlineAt) { this.responseText = responseText; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java index cf5c1b1..7ea452b 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java @@ -82,17 +82,11 @@ public void markSuccess(String slackTs, String slackChannelId) { this.sentAt = LocalDateTime.now(); } - public void markCreatedBy(UUID userId) { - super.markCreated(userId); - } - - public void updateMessage(String newMessage, UUID userId) { + public void updateMessage(String newMessage) { // userId 파라미터 제거 if (newMessage == null || newMessage.isBlank()) { throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_REQUIRED); } - this.message = newMessage; - super.markUpdated(userId); } public void markDeleted(UUID userId) { @@ -117,7 +111,7 @@ public void validateUpdatable() { throw new BusinessException(SlackErrorCode.SLACK_CHANNEL_ID_REQUIRED); } } - + public void validateDeletable() { if (this.getDeletedAt() != null) { throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND); diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java index e06fa99..4f2f801 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -17,11 +18,9 @@ import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiRequest; import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiResponse; -import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; @Component -@RequiredArgsConstructor public class GeminiApiClient implements AiGenerator { private static final String API_KEY_HEADER = "x-goog-api-key"; @@ -32,9 +31,17 @@ public class GeminiApiClient implements AiGenerator { private final WebClient webClient; private final GeminiProperties geminiProperties; + public GeminiApiClient( + @Qualifier("geminiWebClient") WebClient webClient, + GeminiProperties geminiProperties + ) { + this.webClient = webClient; + this.geminiProperties = geminiProperties; + } + @Override public AiResponseInfo generate(String prompt) { - + validatePrompt(prompt); GeminiResponse response; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java index 18f2cab..feb5d47 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java @@ -8,8 +8,8 @@ @Configuration @EnableConfigurationProperties(GeminiProperties.class) public class GeminiApiConfig { - @Bean - public WebClient webClient() { + @Bean(name = "geminiWebClient") + public WebClient geminiWebClient() { return WebClient.builder().build(); } } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java index b851826..faea649 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -@FeignClient(name = "order-service") +@FeignClient(name = "orderservice") public interface OrderInternalClient { @GetMapping("/internal/orders/{orderId}/read-model") diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java index 280bd1f..6130e7a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java @@ -1,12 +1,18 @@ package com.shipflow.notificationservice.infrastructure.client.order; +import java.time.LocalDateTime; import java.util.UUID; public record OrderReadModelResponse( UUID orderId, - String productId, + UUID productId, String productName, + int quantity, + String ordererName, + LocalDateTime createdAt, String departureHubName, - String arrivalHubName + String arrivalHubName, + LocalDateTime requestDeadline, + String requestNote ) { } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java index f7d3545..3db7102 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull; public record GenerateDeadlineRequest( + @NotNull UUID orderId, @NotNull UUID relatedShipmentId, @NotNull UUID shipmentManagerId, @NotNull String receiverSlackId, @@ -27,7 +28,7 @@ public record GenerateDeadlineRequest( ) { public GenerateDeadlineCommand toCommand(UUID ordererId) { return new GenerateDeadlineCommand( - null, + orderId, ordererId, relatedShipmentId, shipmentManagerId, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java index 552b617..65eb12d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java @@ -32,9 +32,7 @@ @RestController @RequestMapping("/api/slack") public class SlackController { - // TODO: Gateway + Keycloak 연동 후 @AuthenticationPrincipal 적용 - // TODO: external API 권한 체크(@PreAuthorize) 추가 - // TODO: internal API 서비스 간 인증 방식 반영 + private final SlackAppService slackAppService; public SlackController(SlackAppService slackAppService) { diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index 29c165e..ac279a9 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -1,12 +1,15 @@ spring: + main: + web-application-type: servlet + application: - name: notification-service + name: notificationservice datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:shipflow}?currentSchema=notification - username: ${DB_USER:shipflow} - password: ${DB_PASSWORD:1234} + url: jdbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432}/${POSTGRES_DB:shipflow}?currentSchema=notification + username: ${POSTGRES_USER:shipflow} + password: ${POSTGRES_PASSWORD} jpa: hibernate: @@ -19,7 +22,7 @@ spring: import: optional:file:.env rabbitmq: - host: ${RABBITMQ_HOST:localhost} + host: ${RABBITMQ_HOST:rabbitmq} port: ${RABBITMQ_PORT:5672} username: ${RABBITMQ_USERNAME:guest} password: ${RABBITMQ_PASSWORD:guest} @@ -32,13 +35,17 @@ gemini: url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent server: - port: 8087 + port: 8080 eureka: client: register-with-eureka: true fetch-registry: true service-url: - defaultZone: http://localhost:8761/eureka/ - instance: - prefer-ip-address: true \ No newline at end of file + defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://discoveryserver:8761/eureka/} + +springdoc: + swagger-ui: + path: /swagger-ui.html + api-docs: + path: /v3/api-docs \ No newline at end of file diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java index cc95960..75a6312 100644 --- a/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java +++ b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java @@ -50,10 +50,15 @@ void handleShipmentCreated_success() { OrderReadModelResponse order = new OrderReadModelResponse( event.getOrderId(), - UUID.randomUUID().toString(), - "마른 오징어", - "경기 북부 센터", - "부산광역시 센터" + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote ); AiLogResult aiResult = mock(AiLogResult.class); @@ -106,10 +111,15 @@ void handleShipmentCreated_nullSlackId() { OrderReadModelResponse order = new OrderReadModelResponse( event.getOrderId(), - UUID.randomUUID().toString(), - "마른 오징어", - "경기 북부 센터", - "부산광역시 센터" + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote ); AiLogResult aiResult = mock(AiLogResult.class); @@ -136,10 +146,15 @@ void handleShipmentCreated_withRoutes() { OrderReadModelResponse order = new OrderReadModelResponse( event.getOrderId(), - UUID.randomUUID().toString(), - "마른 오징어", - "경기 북부 센터", - "부산광역시 센터" + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote ); AiLogResult aiResult = mock(AiLogResult.class); From df5b89a3b2b5be3ac724b30cd3ec1c036c6297d3 Mon Sep 17 00:00:00 2001 From: 250 Date: Tue, 7 Apr 2026 04:29:51 +0900 Subject: [PATCH 16/20] =?UTF-8?q?fix(notification):=20todo=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationservice/application/slack/SlackAppService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 762ed53..d264c46 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -30,7 +30,6 @@ public class SlackAppService { private final SlackSender slackSender; // 메시지 전송 - // TODO: 자동 발송의 경우 receiverSlackId를 사용자/이벤트 정보 기반으로 조회하도록 분리 @Transactional public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { //권한 확인 From 99b5db125b662013fe64d3acf086d176c3dbbfc1 Mon Sep 17 00:00:00 2001 From: 250 Date: Tue, 7 Apr 2026 04:44:47 +0900 Subject: [PATCH 17/20] chore: remove keycloak export from tracking --- .gitignore | 5 + keycloak/shipflow-export.json | 2372 --------------------------------- 2 files changed, 5 insertions(+), 2372 deletions(-) delete mode 100644 keycloak/shipflow-export.json diff --git a/.gitignore b/.gitignore index f22d021..416baa6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ out/ generated/ .planning/ + +keycloak/*-export.json +keycloak/*.jks +keycloak/*.p12 +keycloak/*.pem \ No newline at end of file diff --git a/keycloak/shipflow-export.json b/keycloak/shipflow-export.json deleted file mode 100644 index ad9925d..0000000 --- a/keycloak/shipflow-export.json +++ /dev/null @@ -1,2372 +0,0 @@ -{ - "id": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "realm": "shipflow", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": false, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxTemporaryLockouts": 0, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "9c6ebe92-6809-44ff-b785-61ca2ec674a5", - "name": "COMPANY_MANAGER", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "437b33b6-c49b-4cd6-9e1e-de118ba010ff", - "name": "HUB_MANAGER", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "dbea48c0-3ebf-49e9-acb1-60036fc05185", - "name": "MASTER", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name": "default-roles-shipflow", - "description": "${role_default-roles}", - "composite": false, - "composites": { - "realm": [ - "offline_access", - "uma_authorization" - ], - "client": { - "account": [ - "manage-account", - "view-profile" - ] - } - }, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "d17dde63-adeb-41df-838d-c8ee55622c70", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "febb44ff-b3c0-4870-a9e8-d84afb05bc51", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - }, - { - "id": "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", - "name": "SHIPMENT_MANAGER", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes": {} - } - ], - "client": { - "realm-management": [ - { - "id": "5ac95120-9c91-42fc-a9df-6e38970dad79", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "caa610f3-4986-42d7-b720-6bd8132da76a", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "9e5a736d-34b7-4aac-b8ac-7cd14650f379", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "812427c2-0481-4ba6-9e67-ee98739d7489", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "8c3c30fe-95a8-413e-85b0-0f963e5a643c", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "0d7742bd-b6dc-4ca6-ab61-3f516d540624", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "cab6e153-818c-467f-9bb2-652120c2fe4d", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "ebb9d35e-deb8-4b59-86df-0cef09d4640c", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "5237356a-5e32-4ddb-bac7-4e521a56326e", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "ce72a656-73d8-49c9-a04e-cfefd0e89d13", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-users", - "query-groups" - ] - } - }, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "c5fa6724-d145-484d-8f33-dafa65b13b74", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "d5324b44-3656-4978-bb13-96915cdf60ff", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "ef01e291-c2c5-4d47-bc83-e50a1971ee55", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "34d178dc-ed4b-4517-b261-de55f93c7ab9", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "de1f054b-15ad-4839-a54e-537039a31820", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "manage-realm", - "view-clients", - "manage-clients", - "query-clients", - "view-authorization", - "view-realm", - "manage-events", - "create-client", - "query-realms", - "view-events", - "view-identity-providers", - "view-users", - "query-users", - "manage-authorization", - "manage-identity-providers", - "impersonation", - "manage-users", - "query-groups" - ] - } - }, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - }, - { - "id": "5df84e42-5988-4b59-a8eb-b83835f1f662", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [ - { - "id": "d66bcb04-700d-483c-9628-207d0bd431bd", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "0b06878c-6957-40d0-8783-23e075fcf103", - "attributes": {} - } - ], - "account": [ - { - "id": "044b18e6-b560-43c8-925d-80fbce6ddc28", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": [ - "view-consent" - ] - } - }, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "fa040d5d-ca2f-4386-85bd-75716818b4a0", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "76c4a106-bd5d-43b2-9098-708ea5813f2a", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "c99f7465-dd93-4f8d-ab26-64439d75f0c2", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "43c296b2-6d12-41f8-898a-2663dde41447", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": [ - "manage-account-links" - ] - } - }, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "b48d559d-9f77-4106-afef-8d9ba7c62e2f", - "name": "view-groups", - "description": "${role_view-groups}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - }, - { - "id": "49dce239-8341-44bf-8a1b-09b9992da150", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRole": { - "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name": "default-roles-shipflow", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": [ - "totpAppFreeOTPName", - "totpAppGoogleName", - "totpAppMicrosoftAuthenticatorName" - ], - "localizationTexts": {}, - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyExtraOrigins": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "webAuthnPolicyPasswordlessExtraOrigins": [], - "users": [ - { - "id": "0c6a758d-afe4-47a4-9f09-df82c6e99653", - "username": "master", - "emailVerified": true, - "createdTimestamp": 1775150577528, - "enabled": true, - "totp": false, - "credentials": [ - { - "id": "eef9ff22-fc19-48b5-83c4-7036ffa3c7bb", - "type": "password", - "userLabel": "My password", - "createdDate": 1775150577528, - "secretData": "{\"value\":\"xhuravFV0LOqcWNYBRWJ8fvnc2HVu0KvUwmDouoIYzU=\",\"salt\":\"GE9MOvjB9bf+/tpUY4QNOQ==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "MASTER" - ], - "notBefore": 0, - "groups": [] - } - ], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": [ - "manage-account", - "view-groups" - ] - } - ] - }, - "clients": [ - { - "id": "3e1fcbc4-434d-4104-86b2-206948dae66c", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/shipflow/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/shipflow/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "d944f492-235f-4a2e-8713-5f4893969dcb", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/shipflow/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/shipflow/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "b5c354ab-6b26-437a-960b-3aacde70b086", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "baa30b35-5489-4baf-85e0-a05059153a81", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "0b06878c-6957-40d0-8783-23e075fcf103", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "f1683ac5-4c05-4465-8c45-351b7a533da9", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "clientId": "shipflow-api", - "name": "${login-client-id}", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/*" - ], - "webOrigins": [ - "/*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "login_theme": "", - "display.on.consent.screen": "false", - "consent.screen.text": "", - "frontchannel.logout.url": "", - "backchannel.logout.url": "" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true - }, - "authorizationServicesEnabled": false - }, - { - "id": "39987a2a-1607-481c-a891-e33a14c9c337", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/shipflow/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/admin/shipflow/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "9e3dc333-40f0-457e-b5e7-fdadd440aa78", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "80461e14-093c-4647-b9fb-b7a8fc843ff2", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "abdac8d3-83eb-4993-9c0a-57221ffc55b4", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "33d17aeb-10a0-4f8c-9e6c-afdf595da401", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "17861556-524a-4d33-9e30-d7df5297f61e", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${profileScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "c88cf316-27ec-4940-9b87-614a6125ee3a", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "828bd040-4877-452e-b083-3a658a52853c", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "341a7f66-f7fc-4cac-8584-77dc6444e259", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "6415f321-b218-401d-88c0-492b428cd86c", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "b971d53b-9925-4ffe-a649-4aab9ed3d880", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "09e729d0-d8dc-4693-a693-37d3237aadcd", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "8123f18d-17b8-454e-9eb9-493e3ecf988b", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "041d5689-35c8-4776-ba95-96ca02ac2b2f", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "e800e5a6-90b5-4987-944e-5226110af8cf", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "26f011c7-5681-4e8e-8f1a-d171495b2639", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "dde63b52-45c9-4587-9341-b5e5fe4e75a2", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "f941894d-e133-44ee-8747-698c9b3ffa76", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "d15e02e3-4763-4519-a622-365a2840d37f", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "3d56cee0-2f92-4161-b112-affc02423932", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${phoneScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "18102df6-716f-4670-b62e-f7c072e3b8c5", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": true, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "09913ba8-d796-4445-ad45-e8c13d6b3f6e", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "ab9eb324-487d-44a3-9a61-d05f989bc13b", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "consent.screen.text": "", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "e7480737-d23a-4057-ac0b-4527d5747338", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "148965f8-21a6-4eb0-a062-3c7138f2c351", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "674690ff-249d-434e-8c01-cdc901c02360", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "b40f77db-de92-4c75-b79b-10506658a17f", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "e3ce3daa-b344-4d67-8ff8-374157a39321", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "consent.screen.text": "${rolesScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "e82a1e72-897a-4b29-9138-6047db2d2d55", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - }, - { - "id": "677bda21-6981-4dff-ab3d-2d17011fa95f", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "introspection.token.claim": "true", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "81d25232-91dc-4ceb-903d-46b2d0953ea2", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "introspection.token.claim": "true", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${addressScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "a44b439c-7386-4fee-849f-d7bbc0ce44ba", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "introspection.token.claim": "true", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "d2f34d78-2c96-436e-ac6b-05f34a46de36", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "fea6c421-3e54-499f-9189-38d992452d28", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "consent.screen.text": "${emailScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "9394eaaf-125a-4df9-93d4-c7a309a81606", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", - "name": "basic", - "description": "OpenID Connect scope for add all basic claims to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", - "name": "auth_time", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "AUTH_TIME", - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "claim.name": "auth_time", - "jsonType.label": "long" - } - }, - { - "id": "d067706a-9d23-456b-acb1-253f62f663c5", - "name": "sub", - "protocol": "openid-connect", - "protocolMapper": "oidc-sub-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] - } - ], - "defaultDefaultClientScopes": [ - "role_list", - "profile", - "email", - "roles", - "web-origins", - "acr", - "basic" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "address", - "phone", - "microprofile-jwt" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "referrerPolicy": "no-referrer", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "45cf6edb-f05e-463d-89f0-95dfef49b8e3", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "838220c4-f89b-491b-a1fb-4cf0e9e9be80", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "649a9fd6-d43f-4adf-bc06-fdd97ef6b66e", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-attribute-mapper", - "saml-user-property-mapper", - "saml-user-attribute-mapper", - "saml-role-list-mapper", - "oidc-usermodel-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "oidc-address-mapper" - ] - } - }, - { - "id": "925c2991-c100-4031-b582-6aeb21a4be6d", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "9862faf6-ef7f-4351-901f-f69c4d03e177", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-attribute-mapper", - "saml-user-property-mapper", - "oidc-address-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper" - ] - } - }, - { - "id": "931e5f0e-94b5-4990-88a8-b2e3e37b715e", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "700ee54a-d541-4f30-80c5-feed1a278050", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "d68d4e22-9119-4813-a40c-b72f2018917a", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "c418f251-7d0a-4f89-8020-8e3c3a7fa0b3", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": { - "kc.user.profile.config": [ - "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":4,\"max\":10},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[],\"edit\":[]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[],\"edit\":[]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" - ] - } - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "3c2fef2f-cebe-49f9-bd02-67d506bc8724", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": [ - "430be029-2d11-4edf-bf33-cc6b13d6066d" - ], - "secret": [ - "45L5ziXkEQsQjxvtdSpRIA" - ], - "priority": [ - "100" - ] - } - }, - { - "id": "fca8ba49-3626-4e1f-abaa-298ec4775dbf", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEogIBAAKCAQEAv07QFoU7dJcNIeyAPE0u1y2colgiVHNeoaNcrCsQhfFP11qqRXrPOZL8reKEAFFrC8nHyc7ZjVSjUMUGzcyqpHDxuXM0D2lQGz5OAShfY4QGkbrzqyPBtmhsr8vehnhRWUipNgEP4klZaIOac8rXT7p42fIzKk4fDK6HUTPVcWjphhHcY5tZdLcHYUF4BtFJn5I++DBW5Xkg+cWmd/jX6BVlP84ATYSXnxNDDJx1N+mJ8dnUsjYmt79rCLqwgTVpBMKbIpf9LuIbNTOHm6fV4BN44fDrxMm/xMzT1S0bAUrAhMctTF5/jBw4g/TqVIgkcfcttkik5AfOT84p2t4TKQIDAQABAoIBAAWnNe+2be548LjICPWPN9NAvCxRc6lAdANNhSVqy90TZ4829Qli0skSIoCebRVn0oSZiLt6TvQ11DIkslnmQoQjuMboxDjw3R7C+XHgECqMChgoGL99yeGCpjSPLxMU7t13L9XiU+Z1Uaysl+W0UKbA3UWeejvVrWX16daA1D26xXTqTTR11YbhlgBQxKGQXFYXm9IORNYevAYRSdIl6LiDHHb/NgsgNMF7zVXoiLs87s8+sEZ5XYoJJDCoDMDnXLqUlTyKqouVz3iU5bL8fHHdWO7AfwcbfgiAGHe5Ffskm62k153Ye88R3zYRaAeaifjTHfYrNLFCZY5unZDc4BECgYEA59fNhjcPxDlIAmkb4K028qEAed7RFxNUHcQ5jSScG2OrGr3dVzWdE0I+8u3yn/5PCpVglMSfndr9A3Xu+jttJ5WmNn9QnoqeGiWZsp7GXV9mqbbobPKHzO9Mu9QbVJu6+ZkDUaFaryoOI3zNzLUOAsXN8Vc9ved0vPduAuLryjECgYEA0z3F8sQ6BjEoWNbujuVHI+Sms0gOUIIDJzlF/NB3CvLZGe4yJ5pf7G+omVMTspaxNg48E6PO4NjWAThQW9hZBI1bgR/hKOghbeqF2dP+UEEepoo8KSY6VEIPUxEjyrIWTX0jDJHNnNr72j60wnHHv9Z7laj2qafYo9ECFWxhInkCgYA+xV8QB7htGFU20d6KZluKNa07UeiqpsEPjiFG5bKed83L37wd8JYmsLj6bRJT3zbnVqpfnRzaUIBQf43EknJrVUk7WB0rz7weuC90/SgX/8x8BtnHJaM/CUttT3BW6BMnoRYU8+rpoilR0mimFB9HAOdRgJ1m3VPuFc/jWC0fAQKBgBWH/l04UxG+gPZNMhOumwm1jKhJd+wM1HVzCQcz2G5tQmO6O7J9sblPyEeYiDFz2qw/1y/JSpTwhR+qtcYmzyv/nIwUy8Z3orCpbus9CHb1rEIdZPRsyRU9hoJZBOTsMgnD74agdey/BVzBd3s6TbnoCsC+cCXqzdIkw6mbWmtBAoGAOzbfy/jDwIfpNEoHqHG0IwIbpvked3VAMN64UDXVQJ82M9JcTyqeWl3d89h6YGaOoBAZf4+W3X+V+WNu5ZKgdgmFR8PVyibUVWCZ97KjjnM2tEz4+ovfNC5PiVgDQokpOi6qGCPWresVPK4NSEksoYhpv/I+l3WsifXKUU0hy9c=" - ], - "keyUse": [ - "SIG" - ], - "certificate": [ - "MIICnzCCAYcCBgGdVEmoPTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhzaGlwZmxvdzAeFw0yNjA0MDMxNjU4MjZaFw0zNjA0MDMxNzAwMDZaMBMxETAPBgNVBAMMCHNoaXBmbG93MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv07QFoU7dJcNIeyAPE0u1y2colgiVHNeoaNcrCsQhfFP11qqRXrPOZL8reKEAFFrC8nHyc7ZjVSjUMUGzcyqpHDxuXM0D2lQGz5OAShfY4QGkbrzqyPBtmhsr8vehnhRWUipNgEP4klZaIOac8rXT7p42fIzKk4fDK6HUTPVcWjphhHcY5tZdLcHYUF4BtFJn5I++DBW5Xkg+cWmd/jX6BVlP84ATYSXnxNDDJx1N+mJ8dnUsjYmt79rCLqwgTVpBMKbIpf9LuIbNTOHm6fV4BN44fDrxMm/xMzT1S0bAUrAhMctTF5/jBw4g/TqVIgkcfcttkik5AfOT84p2t4TKQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBaTG52wKWCIO6pkkczu39V+xvsItyyW1PaZjXOWMTzN6jhaYgy1cIQhRf73HVoOyscL23qzbpAe5tSe4NAhuKpuhpewLJ4uifW7vUG1flsDpz+HVU4X96Se7W0RBfOp1pYBwl1CCzT3a94zeBAT0g77b6f6vBB3UUtDi1ONUjxuguI+/7PMIq3UDp49I0D9G/Za8xm4Hl0dXpGCorx77VabBicFU2i5aHLaFQz9uvL5eIoKckutXF9GSoY9vNXpa+Qjcq6wMHsFBI/1veR5iZ31fGHYYYI375VdxiBzqEF8eGuUTf5Ef8ZfnAMdm5HFt09buFX6NOQmVaVogUHFJ1e" - ], - "priority": [ - "100" - ] - } - }, - { - "id": "75ea9636-6875-4464-8066-4d961fb32d6e", - "name": "hmac-generated-hs512", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": [ - "17b83a53-8d14-40a1-b4f2-f895c7489b67" - ], - "secret": [ - "TGPGAJeWFZdtUsJyieamyDJUzMzuiD_KaHdBftdfjQ_6H53rUdgiWIcakE9AOyklFaxc64j-40vKa0qtLWB28OwJ-jYIFgULDg4QZGAHXbZJ12hbf5_NXog5UFQVlB3TkLJ2HNbLgGnJ_eWnyzsVJfZkgvS8t-uhHVjDf9XN6Jo" - ], - "priority": [ - "100" - ], - "algorithm": [ - "HS512" - ] - } - }, - { - "id": "82f584db-f4b2-4fbf-974f-b15ef6a7a6c6", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEpAIBAAKCAQEA3qlBybBF++h5Q/U6o8IS8Ld2YT23MTFLO6h3ZKqzYLsp55rhg4FMoYEIU7L57PCmtkCr4ON/04dayw2OHBPi4UvltK8HTOZnri6j3ACO2SkxO7iV0UwRmjMzYH+rB33OAr6qn/pg2dQp04NHTzgixvvca9y1G4DR1EpnsePQRTRz3INWT+KYokL4INdubvtGdsmv/PedczzXkPSeOWKj/tA7epD0IV0apnDg5vrpgaARGF30zwLYGdWp3pZbmggkXhHLHijUctzEEViKe9YJtS6gH9q329XGT2RCNhIwthPPYV5+eSdsRuIpZ5AXQN+j91FKdBCdVo1lKlbsue0h4QIDAQABAoIBABfeCshE1eufysfnFIcTOZaGA/F+fRGP2CGn+ExZI+s9hMtnxb6j8IPrYeoe6D1mumgU3Je5qy0QeEIFzPLjitFdolzQ2jZ7CCgapcPiZ22NxdJCAgUyYzylOl+gr8OYz6lpqL6HRzRyUp1ymAU83jV4L/N78AnnsBZSd3URF3UjbAS6z6NQfoqDCOxmXiA/iLYGmpB+SQKbjWaxeozsnlgwCbH6EER/aQVlX8vAmt5bFbh3dTAL9pktQFxW9P/LIwUfd1ghEutaNr1r52UEU7w+KyiDDLHaT9Tt5piO9fhMNgkDxtRl3W+PVIt5/nhaQPLcfE4NTnRBlsGzA+a/P9UCgYEA9z7G83QarH90A+HcFkKw+/IT72yZdukN7V+p4W0DSxQX57n5WTy46+sMTbsGdnvCJSP1iOXEGyFQhc8IrrFJXKF9K5gtzQ1dvnLQCwvhaP5awBTS4qm/5dnlOUVW0770mlHucvPVzwgRBz7GEDKbd8xItL+kHB0YvPekI5bwd1MCgYEA5ouheqZtFF3ufxJLUFolTf8RfWYsm8Z5G+O2fEhW62r6ujmDXwk9eNxSRj1SoJzFnNIS3rgd4Y1/XRHfw+mmvFuFC7Ih1ktmPQ9LpcbE02tvIwings4zHTv3Fv1t+qMnk54AuPmT3txTHZx4eVKtOMXIqSUT+HCW+/dOsi/0X3sCgYEAgf4eqjecIp+sRrJEfeu4k+62LobBtTRZXzmR3vTq61l4LByqjhGQBHIDeQbhIgB1lgNu//gWAFGmvYOZxAdwU+SQJBCR3CKv7Ab/fR9U91fsLNuF+ShYvaevjkn3mcLnZg+3t/adrolGMrH9ftyswvLEM0wjI6jkrc3iHdgpPAMCgYEArIB74fbXFW83PfNlUQkycorQ/mBOLnyyL9ERwSqrhtj0JBVWm+yhB2brVM0bnzvOjQmOvwFKsnMagnwWT1Prw3JDOb4enWaraDKiqrbwnTT84lzeYfyBuHUe7B/Sg8BCo6yM49sy7oUy16w1ZKodHKa4/v7UU4eDIaMpSiChnDMCgYAk3Yt2AFbp3hmH9atKa563tZB9niuRVsQKpE2d+/3l2H5H+iRnXXHMSgtix52Cq3F8M3AzS27lzg6bT95B+YBOqYIkNlr9kjAY0rscR0yELNWeRLXeOFVFn9EJEJ7RRtzi1ttnngz4WjdcbFNWoDSqJI6micgCyfhcGU+g9ybHkw==" - ], - "keyUse": [ - "ENC" - ], - "certificate": [ - "MIICnzCCAYcCBgGdVEmpxzANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhzaGlwZmxvdzAeFw0yNjA0MDMxNjU4MjZaFw0zNjA0MDMxNzAwMDZaMBMxETAPBgNVBAMMCHNoaXBmbG93MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3qlBybBF++h5Q/U6o8IS8Ld2YT23MTFLO6h3ZKqzYLsp55rhg4FMoYEIU7L57PCmtkCr4ON/04dayw2OHBPi4UvltK8HTOZnri6j3ACO2SkxO7iV0UwRmjMzYH+rB33OAr6qn/pg2dQp04NHTzgixvvca9y1G4DR1EpnsePQRTRz3INWT+KYokL4INdubvtGdsmv/PedczzXkPSeOWKj/tA7epD0IV0apnDg5vrpgaARGF30zwLYGdWp3pZbmggkXhHLHijUctzEEViKe9YJtS6gH9q329XGT2RCNhIwthPPYV5+eSdsRuIpZ5AXQN+j91FKdBCdVo1lKlbsue0h4QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA5KjW40V6ORkYrbRUDqADxkFePlnRhABnlZREXB2EFcnDjOUFxDlUVlG3j/MLw/EPSbf7HMEk6ZVeBKo7hG3DXb1eYEgob7mgIlkajw7YjiVnNBc0fg/UtpV6qkCGPIPvNMEUIAk2v1wuDIRM82020gmcJWW1nGIh48W9sGIszoelYnPwEq8KeMGljBGvw919lIn7VwzHn5NSf640sI9RHtBw/GOjduV55kGdWYSpcLFjLT/qRC4aejDI+KikW52qdekEhx8iTpbCDigijlViNjlbKSqrfhnYdUNPOxwpff2okE85bSYhejpmPE9zdd/v7CPcQWv0MZOed4zQHLIY3" - ], - "priority": [ - "100" - ], - "algorithm": [ - "RSA-OAEP" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "fda8b12c-9bb9-4c04-860a-5b69202d5714", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "ff955351-02a7-4978-9dc8-3a7c3e46fb91", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "f778c69e-8c53-427c-a299-5d49906aea63", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "d3d5c34e-35cd-49aa-8e03-90dce67080ee", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "9eaf459c-709f-46d7-b312-bb3d01bf561e", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "8a6ccaa6-96a0-45a7-af86-69238b8762ea", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "a726d5c4-022e-475b-bf0d-57e9dcb911fa", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "86523e55-21bf-44c9-b107-6f853f0f168a", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "ceac3589-19df-47f7-ad07-2077ea1fc5d4", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "ba752550-cfc6-48b3-a521-4972685f42b2", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "ed913470-af37-429c-af89-cc1397fb4147", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "295facf5-3aca-4b02-a0cf-360593092a46", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "514b1696-d644-4f01-8b96-e6013de7078c", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-terms-and-conditions", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 70, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "611045a9-bade-435d-b549-b62e497d44a5", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "74141972-4429-4565-b576-26ee4b374060", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "039a1f5a-d31b-4e4e-a401-e02694d1cb26", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "8d580f89-f9cd-4259-ab24-affe163ff5e7", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": false, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "VERIFY_PROFILE", - "name": "Verify Profile", - "providerId": "VERIFY_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 90, - "config": {} - }, - { - "alias": "delete_credential", - "name": "Delete Credential", - "providerId": "delete_credential", - "enabled": true, - "defaultAction": false, - "priority": 100, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "firstBrokerLoginFlow": "first broker login", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DevicePollingInterval": "5", - "clientOfflineSessionMaxLifespan": "0", - "clientSessionIdleTimeout": "0", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5", - "realmReusableOtpCode": "false", - "cibaExpiresIn": "120", - "oauth2DeviceCodeLifespan": "600", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0", - "organizationsEnabled": "false" - }, - "keycloakVersion": "25.0.6", - "userManagedAccessAllowed": false, - "organizationsEnabled": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } -} \ No newline at end of file From 15b085ae325ad4f23c288df214021cbc45a3149d Mon Sep 17 00:00:00 2001 From: 250 Date: Tue, 7 Apr 2026 05:13:02 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix(notification):=20RedisConfig=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A9=B1=EB=93=B1=EC=84=B1=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC,=20WebClient=20=ED=83=80=EC=9E=84=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=A4=EC=A0=95,=20Slack=20ID=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=EC=8B=9D=20=ED=99=95=EC=9E=A5,=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EB=A0=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notification-service/build.gradle | 2 + .../application/slack/SlackAppService.java | 2 +- .../notificationservice/config/JPAConfig.java | 7 +++- .../config/RedisConfig.java | 20 +++++++++ .../client/ai/config/GeminiApiConfig.java | 17 +++++++- .../consumer/ShipmentCreatedHandler.java | 41 +++++++++++++++++-- .../messaging/dto/ShipmentCreatedEvent.java | 1 - .../slack/SlackMessageRepositoryImpl.java | 1 + .../dto/request/SendSlackMessageRequest.java | 2 +- .../src/main/resources/application.yaml | 5 +++ 10 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java diff --git a/notification-service/build.gradle b/notification-service/build.gradle index f4de34f..9659db7 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -64,6 +64,8 @@ dependencies { // 10. Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index d264c46..7be5177 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -122,7 +122,7 @@ public void deleteSlackMessage(UUID userId, String userRole, UUID slackId) { } private void validateCreateRole(String userRole) { - if (!Set.of("MASTER", "HUB_MANAGER", "DELIVERY_MANAGER", "COMPANY_MANAGER").contains(userRole)) { + if (!Set.of("MASTER", "HUB_MANAGER", "SHIPMENT_MANAGER ", "COMPANY_MANAGER").contains(userRole)) { throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); } } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java index 71509ef..2173159 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java @@ -43,12 +43,15 @@ public AuditorAware auditorAware() { try { ServletRequestAttributes attrs = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); - // RabbitMQ 컨슈머 스레드 등 요청 컨텍스트가 없는 경우 → 시스템 UUID + + // 요청 컨텍스트 없는 경우만 SYSTEM_UUID (RabbitMQ 등) if (attrs == null) return Optional.of(SYSTEM_UUID); + String userId = attrs.getRequest().getHeader("X-User-Id"); if (userId == null || userId.isBlank()) - return Optional.of(SYSTEM_UUID); + throw new IllegalStateException("X-User-Id 헤더가 없습니다."); + return Optional.of(UUID.fromString(userId)); } catch (Exception e) { return Optional.of(SYSTEM_UUID); diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java new file mode 100644 index 0000000..4a64371 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java index feb5d47..a208d05 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java @@ -1,15 +1,28 @@ package com.shipflow.notificationservice.infrastructure.client.ai.config; +import java.time.Duration; + import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; +import io.netty.channel.ChannelOption; +import reactor.netty.http.client.HttpClient; + @Configuration @EnableConfigurationProperties(GeminiProperties.class) public class GeminiApiConfig { + @Bean(name = "geminiWebClient") public WebClient geminiWebClient() { - return WebClient.builder().build(); + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .responseTimeout(Duration.ofSeconds(30)); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); } -} +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java index cd4cb3d..ebbd83b 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java @@ -1,21 +1,56 @@ package com.shipflow.notificationservice.infrastructure.messaging.consumer; +import java.time.Duration; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import com.shipflow.common.messaging.handler.AbstractSagaHandler; import com.shipflow.notificationservice.application.NotificationOrchestratorService; +import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Component -@RequiredArgsConstructor public class ShipmentCreatedHandler extends AbstractSagaHandler { private final NotificationOrchestratorService notificationOrchestratorService; + private final RedisTemplate redisTemplate; + private static final Duration IDEMPOTENCY_TTL = Duration.ofHours(24); + + public ShipmentCreatedHandler( + NotificationOrchestratorService notificationOrchestratorService, + RedisTemplate redisTemplate + ) { + this.notificationOrchestratorService = notificationOrchestratorService; + this.redisTemplate = redisTemplate; + } + + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) + public void receive(ShipmentCreatedEvent event) { + handle(event); + } @Override protected void process(ShipmentCreatedEvent event) { - notificationOrchestratorService.handleShipmentCreated(event); + String key = "saga:processed:" + event.getEventId(); + + Boolean isNew = redisTemplate.opsForValue() + .setIfAbsent(key, "1", IDEMPOTENCY_TTL); + + if (Boolean.FALSE.equals(isNew)) { + log.warn("중복 이벤트 무시 eventId={}", event.getEventId()); + return; + } + + try { + notificationOrchestratorService.handleShipmentCreated(event); + } catch (Exception e) { + redisTemplate.delete(key); + throw e; + } } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 5b974d4..e55b85f 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -15,7 +15,6 @@ @Getter @NoArgsConstructor @AllArgsConstructor -@Builder @JsonIgnoreProperties(ignoreUnknown = true) public class ShipmentCreatedEvent extends SagaEvent { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java index e0c4548..facd7f9 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java @@ -53,6 +53,7 @@ public Page search(SearchSlackMessageCommand command, Pageable pag List content = queryFactory .selectFrom(slackMessage) .where(builder) + .orderBy(slackMessage.createdAt.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java index 442fd5b..ca9a5dc 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java @@ -12,7 +12,7 @@ public record SendSlackMessageRequest( @NotBlank - @Pattern(regexp = "^[UC][A-Z0-9]+$", message = "올바른 Slack ID 형식이 아닙니다.") + @Pattern(regexp = "^[UCDG][A-Z0-9]+$", message = "올바른 Slack ID 형식이 아닙니다.") String receiverSlackId, UUID relatedShipmentId, diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index ac279a9..e6413c5 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -21,6 +21,11 @@ spring: config: import: optional:file:.env + data: + redis: + host: localhost + port: 6379 + rabbitmq: host: ${RABBITMQ_HOST:rabbitmq} port: ${RABBITMQ_PORT:5672} From e874c8afd2764f83ad86666707410b42c1e2d6ae Mon Sep 17 00:00:00 2001 From: 250 Date: Tue, 7 Apr 2026 05:24:25 +0900 Subject: [PATCH 19/20] chore(keycloak): restore sanitized realm export and allow in gitignore --- .gitignore | 4 - keycloak/shipflow-export.json | 2143 +++++++++++++++++++++++++++++++++ 2 files changed, 2143 insertions(+), 4 deletions(-) create mode 100644 keycloak/shipflow-export.json diff --git a/.gitignore b/.gitignore index 416baa6..e6d080a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,3 @@ out/ generated/ .planning/ -keycloak/*-export.json -keycloak/*.jks -keycloak/*.p12 -keycloak/*.pem \ No newline at end of file diff --git a/keycloak/shipflow-export.json b/keycloak/shipflow-export.json new file mode 100644 index 0000000..64d232f --- /dev/null +++ b/keycloak/shipflow-export.json @@ -0,0 +1,2143 @@ +{ + "id": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "realm": "shipflow", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": false, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "9c6ebe92-6809-44ff-b785-61ca2ec674a5", + "name": "COMPANY_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "437b33b6-c49b-4cd6-9e1e-de118ba010ff", + "name": "HUB_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "dbea48c0-3ebf-49e9-acb1-60036fc05185", + "name": "MASTER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": false, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d17dde63-adeb-41df-838d-c8ee55622c70", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "febb44ff-b3c0-4870-a9e8-d84afb05bc51", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", + "name": "SHIPMENT_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "5ac95120-9c91-42fc-a9df-6e38970dad79", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "caa610f3-4986-42d7-b720-6bd8132da76a", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "9e5a736d-34b7-4aac-b8ac-7cd14650f379", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "812427c2-0481-4ba6-9e67-ee98739d7489", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "8c3c30fe-95a8-413e-85b0-0f963e5a643c", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d7742bd-b6dc-4ca6-ab61-3f516d540624", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "cab6e153-818c-467f-9bb2-652120c2fe4d", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ebb9d35e-deb8-4b59-86df-0cef09d4640c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5237356a-5e32-4ddb-bac7-4e521a56326e", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ce72a656-73d8-49c9-a04e-cfefd0e89d13", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "c5fa6724-d145-484d-8f33-dafa65b13b74", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "d5324b44-3656-4978-bb13-96915cdf60ff", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ef01e291-c2c5-4d47-bc83-e50a1971ee55", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "34d178dc-ed4b-4517-b261-de55f93c7ab9", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "de1f054b-15ad-4839-a54e-537039a31820", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-clients", + "manage-clients", + "query-clients", + "view-authorization", + "view-realm", + "manage-events", + "create-client", + "query-realms", + "view-events", + "view-identity-providers", + "view-users", + "query-users", + "manage-authorization", + "manage-identity-providers", + "impersonation", + "manage-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5df84e42-5988-4b59-a8eb-b83835f1f662", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "d66bcb04-700d-483c-9628-207d0bd431bd", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "0b06878c-6957-40d0-8783-23e075fcf103", + "attributes": {} + } + ], + "account": [ + { + "id": "044b18e6-b560-43c8-925d-80fbce6ddc28", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "fa040d5d-ca2f-4386-85bd-75716818b4a0", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "76c4a106-bd5d-43b2-9098-708ea5813f2a", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "c99f7465-dd93-4f8d-ab26-64439d75f0c2", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "43c296b2-6d12-41f8-898a-2663dde41447", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "b48d559d-9f77-4106-afef-8d9ba7c62e2f", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "49dce239-8341-44bf-8a1b-09b9992da150", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "d944f492-235f-4a2e-8713-5f4893969dcb", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "b5c354ab-6b26-437a-960b-3aacde70b086", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "baa30b35-5489-4baf-85e0-a05059153a81", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0b06878c-6957-40d0-8783-23e075fcf103", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "clientId": "shipflow-api", + "name": "${login-client-id}", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "login_theme": "", + "display.on.consent.screen": "false", + "consent.screen.text": "", + "frontchannel.logout.url": "", + "backchannel.logout.url": "" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + }, + "authorizationServicesEnabled": false + }, + { + "id": "39987a2a-1607-481c-a891-e33a14c9c337", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/shipflow/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/shipflow/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9e3dc333-40f0-457e-b5e7-fdadd440aa78", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "80461e14-093c-4647-b9fb-b7a8fc843ff2", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "abdac8d3-83eb-4993-9c0a-57221ffc55b4", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "33d17aeb-10a0-4f8c-9e6c-afdf595da401", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "17861556-524a-4d33-9e30-d7df5297f61e", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "c88cf316-27ec-4940-9b87-614a6125ee3a", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "828bd040-4877-452e-b083-3a658a52853c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "341a7f66-f7fc-4cac-8584-77dc6444e259", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "6415f321-b218-401d-88c0-492b428cd86c", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "b971d53b-9925-4ffe-a649-4aab9ed3d880", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "09e729d0-d8dc-4693-a693-37d3237aadcd", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "8123f18d-17b8-454e-9eb9-493e3ecf988b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "041d5689-35c8-4776-ba95-96ca02ac2b2f", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e800e5a6-90b5-4987-944e-5226110af8cf", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "26f011c7-5681-4e8e-8f1a-d171495b2639", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "dde63b52-45c9-4587-9341-b5e5fe4e75a2", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "f941894d-e133-44ee-8747-698c9b3ffa76", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "d15e02e3-4763-4519-a622-365a2840d37f", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "3d56cee0-2f92-4161-b112-affc02423932", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "18102df6-716f-4670-b62e-f7c072e3b8c5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": true, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "09913ba8-d796-4445-ad45-e8c13d6b3f6e", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ab9eb324-487d-44a3-9a61-d05f989bc13b", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e7480737-d23a-4057-ac0b-4527d5747338", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "148965f8-21a6-4eb0-a062-3c7138f2c351", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "674690ff-249d-434e-8c01-cdc901c02360", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "b40f77db-de92-4c75-b79b-10506658a17f", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "e3ce3daa-b344-4d67-8ff8-374157a39321", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e82a1e72-897a-4b29-9138-6047db2d2d55", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "677bda21-6981-4dff-ab3d-2d17011fa95f", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "81d25232-91dc-4ceb-903d-46b2d0953ea2", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "a44b439c-7386-4fee-849f-d7bbc0ce44ba", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "d2f34d78-2c96-436e-ac6b-05f34a46de36", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "fea6c421-3e54-499f-9189-38d992452d28", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "9394eaaf-125a-4df9-93d4-c7a309a81606", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + }, + { + "id": "d067706a-9d23-456b-acb1-253f62f663c5", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "fda8b12c-9bb9-4c04-860a-5b69202d5714", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "ff955351-02a7-4978-9dc8-3a7c3e46fb91", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f778c69e-8c53-427c-a299-5d49906aea63", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d3d5c34e-35cd-49aa-8e03-90dce67080ee", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "9eaf459c-709f-46d7-b312-bb3d01bf561e", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8a6ccaa6-96a0-45a7-af86-69238b8762ea", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "a726d5c4-022e-475b-bf0d-57e9dcb911fa", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "86523e55-21bf-44c9-b107-6f853f0f168a", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "ceac3589-19df-47f7-ad07-2077ea1fc5d4", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ba752550-cfc6-48b3-a521-4972685f42b2", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "ed913470-af37-429c-af89-cc1397fb4147", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "295facf5-3aca-4b02-a0cf-360593092a46", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "514b1696-d644-4f01-8b96-e6013de7078c", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "611045a9-bade-435d-b549-b62e497d44a5", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "74141972-4429-4565-b576-26ee4b374060", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "039a1f5a-d31b-4e4e-a401-e02694d1cb26", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "8d580f89-f9cd-4259-ab24-affe163ff5e7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": false, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "organizationsEnabled": "false" + }, + "keycloakVersion": "25.0.6", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file From c68bb8a0609c85a631ddb270cdba59ef77ec9208 Mon Sep 17 00:00:00 2001 From: 250ghghghgh Date: Tue, 7 Apr 2026 05:31:25 +0900 Subject: [PATCH 20/20] Update notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../notificationservice/application/slack/SlackAppService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 7be5177..99972ee 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -122,7 +122,7 @@ public void deleteSlackMessage(UUID userId, String userRole, UUID slackId) { } private void validateCreateRole(String userRole) { - if (!Set.of("MASTER", "HUB_MANAGER", "SHIPMENT_MANAGER ", "COMPANY_MANAGER").contains(userRole)) { + if (!Set.of("MASTER", "HUB_MANAGER", "SHIPMENT_MANAGER", "COMPANY_MANAGER").contains(userRole)) { throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); } }