From cdc7187fff2a04b977e7ae001ecc084f7d5d999d Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 10:18:02 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feature(order)=20:=20paging,=20sort=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/OrderSearchCondition.java | 16 +++ .../service/OrderQueryService.java | 26 +++++ .../repository/OrderReadModelRepository.java | 4 + .../infrastructure/config/QuerydslConfig.java | 15 +++ .../OrderReadModelQueryRepository.java | 10 ++ .../OrderReadModelQueryRepositoryImpl.java | 94 ++++++++++++++++++ .../OrderReadModelRepositoryImpl.java | 9 ++ .../controller/OrderController.java | 20 ++-- .../dto/OrderReadModelResponse.java | 60 ++++++++++++ .../presentation/dto/OrderSearchRequest.java | 26 +++++ .../application/OrderQueryServiceTest.java | 98 +++++++++++++++++++ .../orderservice/fixture/OrderFixture.java | 18 ++++ .../presentation/OrderControllerTest.java | 49 ++++++++-- 13 files changed, 431 insertions(+), 14 deletions(-) create mode 100644 order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/config/QuerydslConfig.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepository.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepositoryImpl.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderReadModelResponse.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderSearchRequest.java diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java new file mode 100644 index 0000000..7be2e70 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java @@ -0,0 +1,16 @@ +package com.shipflow.orderservice.application.dto; + +import com.shipflow.orderservice.domain.model.OrderStatus; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record OrderSearchCondition( + OrderStatus status, + UUID ordererId, + UUID productId, + UUID supplierCompanyId, + UUID receiverCompanyId, + LocalDateTime createdFrom, + LocalDateTime createdTo +) {} diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java index a47c5bf..d6b8359 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java @@ -1,16 +1,19 @@ package com.shipflow.orderservice.application.service; import com.shipflow.orderservice.application.dto.OrderResult; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.domain.exception.OrderNotFoundException; import com.shipflow.orderservice.domain.model.Order; import com.shipflow.orderservice.domain.model.OrderReadModel; import com.shipflow.orderservice.domain.repository.OrderReadModelRepository; import com.shipflow.orderservice.domain.repository.OrderRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Set; import java.util.UUID; @Service @@ -18,6 +21,10 @@ @Transactional(readOnly = true) public class OrderQueryService { + private static final Set ALLOWED_PAGE_SIZES = Set.of(10, 30, 50); + private static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.DESC, "createdAt"); + private static final Set ALLOWED_SORT_FIELDS = Set.of("createdAt", "updatedAt"); + private final OrderRepository orderRepository; private final OrderReadModelRepository orderReadModelRepository; @@ -49,4 +56,23 @@ public OrderReadModel getReadModel(UUID orderId) { return orderReadModelRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); } + + /** + * 검색 조건, 정렬, 페이지네이션을 적용하여 주문 목록을 조회합니다. + * 허용된 페이지 크기(10·30·50) 이외의 값은 10으로 정규화되며, + * 허용된 정렬 필드(createdAt·updatedAt) 이외의 값은 createdAt DESC로 폴백됩니다. + */ + public Slice searchOrders(OrderSearchCondition condition, Pageable pageable) { + int pageSize = ALLOWED_PAGE_SIZES.contains(pageable.getPageSize()) + ? pageable.getPageSize() + : 10; + + Sort sort = pageable.getSort().isSorted() + && pageable.getSort().stream().allMatch(o -> ALLOWED_SORT_FIELDS.contains(o.getProperty())) + ? pageable.getSort() + : DEFAULT_SORT; + + Pageable normalized = PageRequest.of(pageable.getPageNumber(), pageSize, sort); + return orderReadModelRepository.search(condition, normalized); + } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/repository/OrderReadModelRepository.java b/order-service/src/main/java/com/shipflow/orderservice/domain/repository/OrderReadModelRepository.java index 7b90562..6f5e9f1 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/domain/repository/OrderReadModelRepository.java +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/repository/OrderReadModelRepository.java @@ -1,6 +1,9 @@ package com.shipflow.orderservice.domain.repository; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.domain.model.OrderReadModel; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import java.util.Optional; import java.util.UUID; @@ -8,4 +11,5 @@ public interface OrderReadModelRepository { Optional findById(UUID orderId); OrderReadModel save(OrderReadModel readModel); + Slice search(OrderSearchCondition condition, Pageable pageable); } diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/config/QuerydslConfig.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/config/QuerydslConfig.java new file mode 100644 index 0000000..e9b20fe --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/config/QuerydslConfig.java @@ -0,0 +1,15 @@ +package com.shipflow.orderservice.infrastructure.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { + return new JPAQueryFactory(entityManager); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepository.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepository.java new file mode 100644 index 0000000..a210380 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepository.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.infrastructure.persistence; + +import com.shipflow.orderservice.application.dto.OrderSearchCondition; +import com.shipflow.orderservice.domain.model.OrderReadModel; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface OrderReadModelQueryRepository { + Slice search(OrderSearchCondition condition, Pageable pageable); +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepositoryImpl.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepositoryImpl.java new file mode 100644 index 0000000..1d1d5e8 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelQueryRepositoryImpl.java @@ -0,0 +1,94 @@ +package com.shipflow.orderservice.infrastructure.persistence; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; +import com.shipflow.orderservice.domain.model.OrderReadModel; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class OrderReadModelQueryRepositoryImpl implements OrderReadModelQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Slice search(OrderSearchCondition condition, Pageable pageable) { + QOrderReadModelJpaEntity q = QOrderReadModelJpaEntity.orderReadModelJpaEntity; + BooleanBuilder builder = new BooleanBuilder(); + + // 소프트 딜리트 제외 + builder.and(q.deletedAt.isNull()); + + // 검색 조건 (null이면 추가 안 함) + if (condition.status() != null) { + builder.and(q.orderStatus.eq(condition.status())); + } + if (condition.ordererId() != null) { + builder.and(q.ordererId.eq(condition.ordererId())); + } + if (condition.productId() != null) { + builder.and(q.productId.eq(condition.productId())); + } + if (condition.supplierCompanyId() != null) { + builder.and(q.supplierCompanyId.eq(condition.supplierCompanyId())); + } + if (condition.receiverCompanyId() != null) { + builder.and(q.receiverCompanyId.eq(condition.receiverCompanyId())); + } + if (condition.createdFrom() != null) { + builder.and(q.createdAt.goe(condition.createdFrom())); + } + if (condition.createdTo() != null) { + builder.and(q.createdAt.loe(condition.createdTo())); + } + + OrderSpecifier[] orderSpecifiers = toOrderSpecifiers(pageable.getSort(), q); + + // Slice 패턴: pageSize+1개 조회 후 hasNext 판단 + List rows = queryFactory + .selectFrom(q) + .where(builder) + .orderBy(orderSpecifiers) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + boolean hasNext = rows.size() > pageable.getPageSize(); + if (hasNext) { + rows = rows.subList(0, pageable.getPageSize()); + } + + List content = rows.stream() + .map(OrderReadModelJpaEntity::toDomain) + .toList(); + + return new SliceImpl<>(content, pageable, hasNext); + } + + private OrderSpecifier[] toOrderSpecifiers(Sort sort, QOrderReadModelJpaEntity q) { + List> specifiers = new ArrayList<>(); + for (Sort.Order order : sort) { + boolean asc = order.isAscending(); + OrderSpecifier spec = switch (order.getProperty()) { + case "createdAt" -> asc ? q.createdAt.asc() : q.createdAt.desc(); + case "updatedAt" -> asc ? q.updatedAt.asc() : q.updatedAt.desc(); + default -> q.createdAt.desc(); + }; + specifiers.add(spec); + } + if (specifiers.isEmpty()) { + specifiers.add(q.createdAt.desc()); + } + return specifiers.toArray(new OrderSpecifier[0]); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelRepositoryImpl.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelRepositoryImpl.java index 8f92bb8..316bb27 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelRepositoryImpl.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderReadModelRepositoryImpl.java @@ -1,8 +1,11 @@ package com.shipflow.orderservice.infrastructure.persistence; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.domain.model.OrderReadModel; import com.shipflow.orderservice.domain.repository.OrderReadModelRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -13,6 +16,7 @@ public class OrderReadModelRepositoryImpl implements OrderReadModelRepository { private final OrderReadModelJpaRepository jpaRepository; + private final OrderReadModelQueryRepository queryRepository; @Override public Optional findById(UUID orderId) { @@ -24,4 +28,9 @@ public OrderReadModel save(OrderReadModel readModel) { OrderReadModelJpaEntity saved = jpaRepository.save(OrderReadModelJpaEntity.from(readModel)); return saved.toDomain(); } + + @Override + public Slice search(OrderSearchCondition condition, Pageable pageable) { + return queryRepository.search(condition, pageable); + } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java index 5cfb3d3..189a435 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java @@ -3,16 +3,20 @@ import com.shipflow.orderservice.application.dto.OrderResult; import com.shipflow.orderservice.application.service.OrderCommandService; import com.shipflow.orderservice.application.service.OrderQueryService; +import com.shipflow.orderservice.domain.model.OrderReadModel; import com.shipflow.orderservice.infrastructure.web.UserContext; import com.shipflow.orderservice.presentation.dto.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.UUID; @RestController @@ -40,11 +44,15 @@ public ResponseEntity getOrder(@PathVariable UUID orderId) { } @GetMapping - public ResponseEntity> getOrders() { - List responses = orderQueryService.getOrders().stream() - .map(OrderResponse::from) - .toList(); - return ResponseEntity.ok(responses); + public ResponseEntity> getOrders( + @ModelAttribute OrderSearchRequest searchRequest, + @PageableDefault(size = 10, page = 0, sort = "createdAt", + direction = Sort.Direction.DESC) Pageable pageable + ) { + Slice result = orderQueryService.searchOrders( + searchRequest.toCondition(), pageable + ); + return ResponseEntity.ok(result.map(OrderReadModelResponse::from)); } @PatchMapping("/{orderId}") diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderReadModelResponse.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderReadModelResponse.java new file mode 100644 index 0000000..bf9e536 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderReadModelResponse.java @@ -0,0 +1,60 @@ +package com.shipflow.orderservice.presentation.dto; + +import com.shipflow.orderservice.domain.model.OrderReadModel; +import com.shipflow.orderservice.domain.model.OrderStatus; +import com.shipflow.orderservice.domain.model.ShipmentStatus; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record OrderReadModelResponse( + UUID orderId, + OrderStatus orderStatus, + UUID ordererId, + String ordererName, + UUID productId, + String productName, + int quantity, + UUID supplierCompanyId, + String supplierCompanyName, + UUID receiverCompanyId, + String receiverCompanyName, + UUID shipmentId, + ShipmentStatus shipmentStatus, + UUID departureHubId, + String departureHubName, + UUID arrivalHubId, + String arrivalHubName, + LocalDateTime requestDeadline, + String requestNote, + String cancelReason, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static OrderReadModelResponse from(OrderReadModel model) { + return new OrderReadModelResponse( + model.getOrderId(), + model.getOrderStatus(), + model.getOrdererId(), + model.getOrdererName(), + model.getProductId(), + model.getProductName(), + model.getQuantity(), + model.getSupplierCompanyId(), + model.getSupplierCompanyName(), + model.getReceiverCompanyId(), + model.getReceiverCompanyName(), + model.getShipmentId(), + model.getShipmentStatus(), + model.getDepartureHubId(), + model.getDepartureHubName(), + model.getArrivalHubId(), + model.getArrivalHubName(), + model.getRequestDeadline(), + model.getRequestNote(), + model.getCancelReason(), + model.getCreatedAt(), + model.getUpdatedAt() + ); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderSearchRequest.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderSearchRequest.java new file mode 100644 index 0000000..08f2430 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/OrderSearchRequest.java @@ -0,0 +1,26 @@ +package com.shipflow.orderservice.presentation.dto; + +import com.shipflow.orderservice.application.dto.OrderSearchCondition; +import com.shipflow.orderservice.domain.model.OrderStatus; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record OrderSearchRequest( + OrderStatus status, + UUID ordererId, + UUID productId, + UUID supplierCompanyId, + UUID receiverCompanyId, + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdFrom, + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdTo +) { + public OrderSearchCondition toCondition() { + return new OrderSearchCondition( + status, ordererId, productId, + supplierCompanyId, receiverCompanyId, + createdFrom, createdTo + ); + } +} diff --git a/order-service/src/test/java/com/shipflow/orderservice/application/OrderQueryServiceTest.java b/order-service/src/test/java/com/shipflow/orderservice/application/OrderQueryServiceTest.java index bf100e2..7bc7315 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/application/OrderQueryServiceTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/application/OrderQueryServiceTest.java @@ -1,17 +1,21 @@ package com.shipflow.orderservice.application; import com.shipflow.orderservice.application.dto.OrderResult; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.application.service.OrderQueryService; import com.shipflow.orderservice.domain.exception.OrderNotFoundException; import com.shipflow.orderservice.domain.model.Order; +import com.shipflow.orderservice.domain.model.OrderReadModel; import com.shipflow.orderservice.domain.repository.OrderReadModelRepository; import com.shipflow.orderservice.domain.repository.OrderRepository; import com.shipflow.orderservice.fixture.OrderFixture; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.*; import java.util.List; import java.util.Optional; @@ -19,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -67,4 +72,97 @@ class OrderQueryServiceTest { assertThatThrownBy(() -> orderQueryService.getReadModel(orderId)) .isInstanceOf(OrderNotFoundException.class); } + + // ───────────────────────────────────── + // searchOrders 테스트 + // ───────────────────────────────────── + + @Test + void searchOrders_빈조건_pageSize10_정상반환() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")); + Slice fakeSlice = new SliceImpl<>( + List.of(OrderFixture.orderReadModel(orderId)), pageable, false + ); + when(orderReadModelRepository.search(any(), any())).thenReturn(fakeSlice); + + Slice result = orderQueryService.searchOrders(condition, pageable); + + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getOrderId()).isEqualTo(orderId); + } + + @Test + void searchOrders_허용되지않은pageSize25_10으로정규화() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 25, Sort.by(Sort.Direction.DESC, "createdAt")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + when(orderReadModelRepository.search(any(), captor.capture())) + .thenReturn(new SliceImpl<>(List.of(), PageRequest.of(0, 10), false)); + + orderQueryService.searchOrders(condition, pageable); + + assertThat(captor.getValue().getPageSize()).isEqualTo(10); + } + + @Test + void searchOrders_허용된pageSize30_그대로전달() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 30, Sort.by(Sort.Direction.DESC, "createdAt")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + when(orderReadModelRepository.search(any(), captor.capture())) + .thenReturn(new SliceImpl<>(List.of(), pageable, false)); + + orderQueryService.searchOrders(condition, pageable); + + assertThat(captor.getValue().getPageSize()).isEqualTo(30); + } + + @Test + void searchOrders_허용된pageSize50_그대로전달() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 50, Sort.by(Sort.Direction.DESC, "createdAt")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + when(orderReadModelRepository.search(any(), captor.capture())) + .thenReturn(new SliceImpl<>(List.of(), pageable, false)); + + orderQueryService.searchOrders(condition, pageable); + + assertThat(captor.getValue().getPageSize()).isEqualTo(50); + } + + @Test + void searchOrders_유효하지않은정렬필드_createdAtDESC폴백() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "productId")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + when(orderReadModelRepository.search(any(), captor.capture())) + .thenReturn(new SliceImpl<>(List.of(), PageRequest.of(0, 10), false)); + + orderQueryService.searchOrders(condition, pageable); + + Sort.Order captured = captor.getValue().getSort().iterator().next(); + assertThat(captured.getProperty()).isEqualTo("createdAt"); + assertThat(captured.getDirection()).isEqualTo(Sort.Direction.DESC); + } + + @Test + void searchOrders_유효한정렬updatedAtASC_그대로전달() { + OrderSearchCondition condition = new OrderSearchCondition(null, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "updatedAt")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + when(orderReadModelRepository.search(any(), captor.capture())) + .thenReturn(new SliceImpl<>(List.of(), pageable, false)); + + orderQueryService.searchOrders(condition, pageable); + + Sort.Order captured = captor.getValue().getSort().iterator().next(); + assertThat(captured.getProperty()).isEqualTo("updatedAt"); + assertThat(captured.getDirection()).isEqualTo(Sort.Direction.ASC); + } } diff --git a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java index 49f8fbe..1754f71 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java +++ b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java @@ -83,6 +83,24 @@ public static Order createdOrder(UUID orderId) { ); } + public static OrderReadModel orderReadModelWithStatus(UUID orderId, OrderStatus status) { + return OrderReadModel.builder() + .orderId(orderId) + .orderStatus(status) + .ordererId(USER_ID) + .productId(PRODUCT_ID) + .supplierCompanyId(SUPPLIER_ID) + .receiverCompanyId(RECEIVER_ID) + .departureHubId(DEP_HUB_ID) + .arrivalHubId(ARR_HUB_ID) + .quantity(10) + .requestDeadline(DEADLINE) + .requestNote("테스트 메모") + .createdBy(USER_ID) + .createdAt(LocalDateTime.of(2026, 4, 1, 9, 0)) + .build(); + } + public static OrderReadModel createdOrderReadModel(UUID orderId) { return OrderReadModel.builder() .orderId(orderId) diff --git a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java index 32faa45..0eeaf94 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java @@ -1,15 +1,20 @@ package com.shipflow.orderservice.presentation; import com.fasterxml.jackson.databind.ObjectMapper; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.application.service.OrderCommandService; import com.shipflow.orderservice.application.service.OrderQueryService; import com.shipflow.orderservice.domain.exception.OrderNotFoundException; +import com.shipflow.orderservice.domain.model.OrderReadModel; +import com.shipflow.orderservice.domain.model.OrderStatus; import com.shipflow.orderservice.fixture.OrderFixture; import com.shipflow.orderservice.infrastructure.web.UserContext; import com.shipflow.orderservice.presentation.controller.OrderController; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.*; import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -17,6 +22,7 @@ import java.util.List; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -120,20 +126,47 @@ class OrderControllerTest { } // ───────────────────────────────────────────── - // GET /api/orders + // GET /api/orders (검색/정렬/페이지네이션) // ───────────────────────────────────────────── @Test - void getOrders_성공_200반환() throws Exception { - when(orderQueryService.getOrders()) - .thenReturn(List.of( - OrderFixture.orderResult(orderId), - OrderFixture.orderResult(UUID.randomUUID()) - )); + void getOrders_기본요청_200반환() throws Exception { + OrderReadModel model = OrderFixture.orderReadModel(orderId); + Slice slice = new SliceImpl<>( + List.of(model), + PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")), + false + ); + when(orderQueryService.searchOrders(any(), any())).thenReturn(slice); mockMvc.perform(get("/api/orders")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(2)); + .andExpect(jsonPath("$.content[0].orderId").value(orderId.toString())) + .andExpect(jsonPath("$.content[0].orderStatus").value("CREATING")); + } + + @Test + void getOrders_상태필터CREATED_파라미터전달됨() throws Exception { + Slice slice = new SliceImpl<>(List.of(), + PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")), false); + ArgumentCaptor conditionCaptor = + ArgumentCaptor.forClass(OrderSearchCondition.class); + when(orderQueryService.searchOrders(conditionCaptor.capture(), any())).thenReturn(slice); + + mockMvc.perform(get("/api/orders").param("status", "CREATED")) + .andExpect(status().isOk()); + + assertThat(conditionCaptor.getValue().status()).isEqualTo(OrderStatus.CREATED); + } + + @Test + void getOrders_페이지네이션_파라미터전달됨() throws Exception { + Slice slice = new SliceImpl<>(List.of(), + PageRequest.of(1, 30, Sort.by(Sort.Direction.DESC, "createdAt")), false); + when(orderQueryService.searchOrders(any(), any())).thenReturn(slice); + + mockMvc.perform(get("/api/orders").param("page", "1").param("size", "30")) + .andExpect(status().isOk()); } // ───────────────────────────────────────────── From 2aed57ac1f8bf40a314758e2097cef5a4f9335c3 Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 12:17:20 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feature(order)=20:=20FeignClient=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=82=B4=EB=B6=80=20=ED=86=B5=EC=8B=A0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- order-service/build.gradle | 5 ++ .../orderservice/OrderserviceApplication.java | 4 ++ .../application/dto/CreateOrderCommand.java | 4 ++ .../service/OrderCommandService.java | 18 ++++-- .../service/OrderFetchService.java | 57 +++++++++++++++++++ .../service/OrderProjectionService.java | 4 ++ .../domain/event/OrderCreatingEvent.java | 4 ++ .../exception/CompanyNotFoundException.java | 10 ++++ .../exception/ExternalServiceException.java | 10 ++++ .../exception/InsufficientStockException.java | 10 ++++ .../domain/exception/OrderErrorCode.java | 7 ++- .../exception/ProductNotFoundException.java | 10 ++++ .../exception/UserNotFoundException.java | 10 ++++ .../client/CompanyFeignClient.java | 19 +++++++ .../client/ProductFeignClient.java | 23 ++++++++ .../client/UserFeignClient.java | 19 +++++++ .../client/adapter/CompanyClientAdapter.java | 36 ++++++++++++ .../client/adapter/ProductClientAdapter.java | 41 +++++++++++++ .../client/adapter/UserClientAdapter.java | 36 ++++++++++++ .../client/dto/ProductInfo.java | 12 ++++ .../client/dto/ReceiverCompanyInfo.java | 9 +++ .../infrastructure/client/dto/UserInfo.java | 9 +++ .../controller/OrderController.java | 4 +- .../presentation/dto/CreateOrderRequest.java | 14 ----- 24 files changed, 353 insertions(+), 22 deletions(-) create mode 100644 order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/CompanyNotFoundException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/ExternalServiceException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/InsufficientStockException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/ProductNotFoundException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/UserNotFoundException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/CompanyFeignClient.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/ProductFeignClient.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/UserFeignClient.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ProductInfo.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/UserInfo.java diff --git a/order-service/build.gradle b/order-service/build.gradle index c93498d..bb3a6b7 100644 --- a/order-service/build.gradle +++ b/order-service/build.gradle @@ -45,6 +45,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-amqp' + // 6. OpenFeign + Retry + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework.boot:spring-boot-starter-aop' + // 5. Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/order-service/src/main/java/com/shipflow/orderservice/OrderserviceApplication.java b/order-service/src/main/java/com/shipflow/orderservice/OrderserviceApplication.java index f150479..a362fe7 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/OrderserviceApplication.java +++ b/order-service/src/main/java/com/shipflow/orderservice/OrderserviceApplication.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.retry.annotation.EnableRetry; @SpringBootApplication(scanBasePackages = "com.shipflow") +@EnableFeignClients(basePackages = "com.shipflow.orderservice.infrastructure.client") +@EnableRetry public class OrderserviceApplication { public static void main(String[] args) { diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java b/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java index a465b83..fd09fe9 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java @@ -5,9 +5,13 @@ public record CreateOrderCommand( UUID ordererId, + String ordererName, UUID productId, + String productName, UUID supplierCompanyId, + String supplierCompanyName, UUID receiverCompanyId, + String receiverCompanyName, UUID departureHubId, UUID arrivalHubId, int quantity, diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java index f338c1f..7422384 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java @@ -4,6 +4,7 @@ import com.shipflow.orderservice.application.dto.CreateOrderCommand; import com.shipflow.orderservice.application.dto.OrderResult; import com.shipflow.orderservice.application.dto.UpdateOrderCommand; +import com.shipflow.orderservice.presentation.dto.CreateOrderRequest; import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.orderservice.domain.event.*; import com.shipflow.orderservice.domain.model.ShipmentStatus; @@ -29,8 +30,14 @@ public class OrderCommandService { private final OrderRepository orderRepository; private final EventPublisher rabbitPublisher; private final ApplicationEventPublisher domainEventPublisher; + private final OrderFetchService orderFetchService; + + public OrderResult createOrder(CreateOrderRequest request, UUID ordererId) { + CreateOrderCommand cmd = orderFetchService.fetchAndBuild( + ordererId, request.productId(), request.quantity(), + request.requestDeadline(), request.requestNote() + ); - public OrderResult createOrder(CreateOrderCommand cmd, UUID requesterId) { Order order = Order.create( cmd.ordererId(), cmd.productId(), @@ -39,14 +46,15 @@ public OrderResult createOrder(CreateOrderCommand cmd, UUID requesterId) { new Quantity(cmd.quantity()), cmd.requestDeadline(), cmd.requestNote(), - requesterId + ordererId ); Order saved = orderRepository.save(order); domainEventPublisher.publishEvent(new OrderCreatingEvent( - saved.getId(), saved.getOrdererId(), saved.getProductId(), - saved.getCompanyInfo().getSupplierCompanyId(), - saved.getCompanyInfo().getReceiverCompanyId(), + saved.getId(), saved.getOrdererId(), cmd.ordererName(), + saved.getProductId(), cmd.productName(), + saved.getCompanyInfo().getSupplierCompanyId(), cmd.supplierCompanyName(), + saved.getCompanyInfo().getReceiverCompanyId(), cmd.receiverCompanyName(), saved.getHubInfo().getDepartureHubId(), saved.getHubInfo().getArrivalHubId(), saved.getQuantity().getValue(), diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java new file mode 100644 index 0000000..c722f50 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java @@ -0,0 +1,57 @@ +package com.shipflow.orderservice.application.service; + +import com.shipflow.orderservice.application.dto.CreateOrderCommand; +import com.shipflow.orderservice.infrastructure.client.adapter.CompanyClientAdapter; +import com.shipflow.orderservice.infrastructure.client.adapter.ProductClientAdapter; +import com.shipflow.orderservice.infrastructure.client.adapter.UserClientAdapter; +import com.shipflow.orderservice.infrastructure.client.dto.ProductInfo; +import com.shipflow.orderservice.infrastructure.client.dto.ReceiverCompanyInfo; +import com.shipflow.orderservice.infrastructure.client.dto.UserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Service +@RequiredArgsConstructor +public class OrderFetchService { + + private final ProductClientAdapter productAdapter; + private final UserClientAdapter userAdapter; + private final CompanyClientAdapter companyAdapter; + + public CreateOrderCommand fetchAndBuild(UUID ordererId, UUID productId, + int quantity, LocalDateTime deadline, String note) { + // Step 1: product, user 병렬 호출 + CompletableFuture productFuture = CompletableFuture.supplyAsync( + () -> productAdapter.fetch(ordererId.toString(), productId, quantity)); + CompletableFuture userFuture = CompletableFuture.supplyAsync( + () -> userAdapter.fetch(ordererId)); + + CompletableFuture.allOf(productFuture, userFuture).join(); + + ProductInfo product = productFuture.join(); + UserInfo user = userFuture.join(); + + // Step 2: receiverCompanyId 확보 후 company 호출 + ReceiverCompanyInfo company = companyAdapter.fetch(user.receiverCompanyId()); + + return new CreateOrderCommand( + ordererId, + user.ordererName(), + productId, + product.productName(), + product.supplierCompanyId(), + product.supplierCompanyName(), + user.receiverCompanyId(), + company.companyName(), + product.departureHubId(), + company.hubId(), + quantity, + deadline, + note + ); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderProjectionService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderProjectionService.java index 7ea78d4..68eb054 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderProjectionService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderProjectionService.java @@ -24,9 +24,13 @@ public void on(OrderCreatingEvent e) { .orderId(e.orderId()) .orderStatus(OrderStatus.CREATING) .ordererId(e.ordererId()) + .ordererName(e.ordererName()) .productId(e.productId()) + .productName(e.productName()) .supplierCompanyId(e.supplierCompanyId()) + .supplierCompanyName(e.supplierCompanyName()) .receiverCompanyId(e.receiverCompanyId()) + .receiverCompanyName(e.receiverCompanyName()) .departureHubId(e.departureHubId()) .arrivalHubId(e.arrivalHubId()) .quantity(e.quantity()) diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/event/OrderCreatingEvent.java b/order-service/src/main/java/com/shipflow/orderservice/domain/event/OrderCreatingEvent.java index f3a2b09..58c3f67 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/domain/event/OrderCreatingEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/event/OrderCreatingEvent.java @@ -6,9 +6,13 @@ public record OrderCreatingEvent( UUID orderId, UUID ordererId, + String ordererName, UUID productId, + String productName, UUID supplierCompanyId, + String supplierCompanyName, UUID receiverCompanyId, + String receiverCompanyName, UUID departureHubId, UUID arrivalHubId, int quantity, diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/CompanyNotFoundException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/CompanyNotFoundException.java new file mode 100644 index 0000000..6e90aee --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/CompanyNotFoundException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class CompanyNotFoundException extends BusinessException { + + public CompanyNotFoundException() { + super(OrderErrorCode.COMPANY_NOT_FOUND); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ExternalServiceException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ExternalServiceException.java new file mode 100644 index 0000000..8535cef --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ExternalServiceException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class ExternalServiceException extends BusinessException { + + public ExternalServiceException(Throwable cause) { + super(OrderErrorCode.EXTERNAL_SERVICE_ERROR, cause); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/InsufficientStockException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/InsufficientStockException.java new file mode 100644 index 0000000..819c572 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/InsufficientStockException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class InsufficientStockException extends BusinessException { + + public InsufficientStockException() { + super(OrderErrorCode.PRODUCT_INSUFFICIENT_STOCK); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java index 2c3ac7f..994a000 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java @@ -6,7 +6,12 @@ public enum OrderErrorCode implements ErrorCode { ORDER_NOT_FOUND("ORDER_NOT_FOUND", HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), - INVALID_ORDER_STATE("INVALID_ORDER_STATE", HttpStatus.CONFLICT, "유효하지 않은 주문 상태입니다."); + INVALID_ORDER_STATE("INVALID_ORDER_STATE", HttpStatus.CONFLICT, "유효하지 않은 주문 상태입니다."), + PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), + PRODUCT_INSUFFICIENT_STOCK("PRODUCT_INSUFFICIENT_STOCK", HttpStatus.CONFLICT, "재고가 부족합니다."), + USER_NOT_FOUND("USER_NOT_FOUND", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + COMPANY_NOT_FOUND("COMPANY_NOT_FOUND", HttpStatus.NOT_FOUND, "업체를 찾을 수 없습니다."), + EXTERNAL_SERVICE_ERROR("EXTERNAL_SERVICE_ERROR", HttpStatus.SERVICE_UNAVAILABLE, "외부 서비스 오류가 발생했습니다."); private final String code; private final HttpStatus status; diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ProductNotFoundException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ProductNotFoundException.java new file mode 100644 index 0000000..2b515c2 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/ProductNotFoundException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class ProductNotFoundException extends BusinessException { + + public ProductNotFoundException() { + super(OrderErrorCode.PRODUCT_NOT_FOUND); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UserNotFoundException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UserNotFoundException.java new file mode 100644 index 0000000..f47ab82 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UserNotFoundException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class UserNotFoundException extends BusinessException { + + public UserNotFoundException() { + super(OrderErrorCode.USER_NOT_FOUND); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/CompanyFeignClient.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/CompanyFeignClient.java new file mode 100644 index 0000000..fd4fb6a --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/CompanyFeignClient.java @@ -0,0 +1,19 @@ +package com.shipflow.orderservice.infrastructure.client; + +import com.shipflow.orderservice.infrastructure.client.dto.ReceiverCompanyInfo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.UUID; + +@FeignClient(name = "companyservice") +public interface CompanyFeignClient { + + @GetMapping("/internal/companies/{companyId}") + ReceiverCompanyInfo getCompanyInfo( + @RequestHeader("X-Internal-Request") String internalRequest, + @PathVariable("companyId") UUID companyId + ); +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/ProductFeignClient.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/ProductFeignClient.java new file mode 100644 index 0000000..a161d79 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/ProductFeignClient.java @@ -0,0 +1,23 @@ +package com.shipflow.orderservice.infrastructure.client; + +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.orderservice.infrastructure.client.dto.ProductInfo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.UUID; + +@FeignClient(name = "productservice") +public interface ProductFeignClient { + + @GetMapping("/internal/products/{productId}") + ApiResponse getProductInfo( + @RequestHeader("X-Internal-Request") String internalRequest, + @RequestHeader("X-User-Id") String userId, + @PathVariable("productId") UUID productId, + @RequestParam("quantity") int quantity + ); +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/UserFeignClient.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/UserFeignClient.java new file mode 100644 index 0000000..bd7e188 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/UserFeignClient.java @@ -0,0 +1,19 @@ +package com.shipflow.orderservice.infrastructure.client; + +import com.shipflow.orderservice.infrastructure.client.dto.UserInfo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.UUID; + +@FeignClient(name = "userservice") +public interface UserFeignClient { + + @GetMapping("/internal/users/{userId}") + UserInfo getUserInfo( + @RequestHeader("X-Internal-Request") String internalRequest, + @PathVariable("userId") UUID userId + ); +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java new file mode 100644 index 0000000..56375dd --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java @@ -0,0 +1,36 @@ +package com.shipflow.orderservice.infrastructure.client.adapter; + +import com.shipflow.orderservice.domain.exception.CompanyNotFoundException; +import com.shipflow.orderservice.domain.exception.ExternalServiceException; +import com.shipflow.orderservice.infrastructure.client.CompanyFeignClient; +import com.shipflow.orderservice.infrastructure.client.dto.ReceiverCompanyInfo; +import feign.RetryableException; +import lombok.RequiredArgsConstructor; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class CompanyClientAdapter { + + private final CompanyFeignClient companyFeignClient; + + @Retryable( + retryFor = {RetryableException.class}, + noRetryFor = {CompanyNotFoundException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 500, multiplier = 2) + ) + public ReceiverCompanyInfo fetch(UUID companyId) { + try { + return companyFeignClient.getCompanyInfo("true", companyId); + } catch (feign.FeignException.NotFound e) { + throw new CompanyNotFoundException(); + } catch (feign.FeignException e) { + throw new ExternalServiceException(e); + } + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java new file mode 100644 index 0000000..26edecb --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java @@ -0,0 +1,41 @@ +package com.shipflow.orderservice.infrastructure.client.adapter; + +import com.shipflow.orderservice.domain.exception.ExternalServiceException; +import com.shipflow.orderservice.domain.exception.InsufficientStockException; +import com.shipflow.orderservice.domain.exception.ProductNotFoundException; +import com.shipflow.orderservice.infrastructure.client.ProductFeignClient; +import com.shipflow.orderservice.infrastructure.client.dto.ProductInfo; +import feign.RetryableException; +import lombok.RequiredArgsConstructor; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class ProductClientAdapter { + + private final ProductFeignClient productFeignClient; + + @Retryable( + retryFor = {RetryableException.class}, + noRetryFor = {ProductNotFoundException.class, InsufficientStockException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 500, multiplier = 2) + ) + public ProductInfo fetch(String ordererId, UUID productId, int quantity) { + try { + ProductInfo info = productFeignClient.getProductInfo("true", ordererId, productId, quantity).getData(); + if (info.stock() < quantity) { + throw new InsufficientStockException(); + } + return info; + } catch (feign.FeignException.NotFound e) { + throw new ProductNotFoundException(); + } catch (feign.FeignException e) { + throw new ExternalServiceException(e); + } + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java new file mode 100644 index 0000000..ee6895a --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java @@ -0,0 +1,36 @@ +package com.shipflow.orderservice.infrastructure.client.adapter; + +import com.shipflow.orderservice.domain.exception.ExternalServiceException; +import com.shipflow.orderservice.domain.exception.UserNotFoundException; +import com.shipflow.orderservice.infrastructure.client.UserFeignClient; +import com.shipflow.orderservice.infrastructure.client.dto.UserInfo; +import feign.RetryableException; +import lombok.RequiredArgsConstructor; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class UserClientAdapter { + + private final UserFeignClient userFeignClient; + + @Retryable( + retryFor = {RetryableException.class}, + noRetryFor = {UserNotFoundException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 500, multiplier = 2) + ) + public UserInfo fetch(UUID userId) { + try { + return userFeignClient.getUserInfo("true", userId); + } catch (feign.FeignException.NotFound e) { + throw new UserNotFoundException(); + } catch (feign.FeignException e) { + throw new ExternalServiceException(e); + } + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ProductInfo.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ProductInfo.java new file mode 100644 index 0000000..06b4b00 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ProductInfo.java @@ -0,0 +1,12 @@ +package com.shipflow.orderservice.infrastructure.client.dto; + +import java.util.UUID; + +public record ProductInfo( + UUID productId, + String productName, + UUID supplierCompanyId, + String supplierCompanyName, + UUID departureHubId, + Integer stock +) {} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java new file mode 100644 index 0000000..1cfccb2 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java @@ -0,0 +1,9 @@ +package com.shipflow.orderservice.infrastructure.client.dto; + +import java.util.UUID; + +public record ReceiverCompanyInfo( + UUID companyId, + String companyName, + UUID hubId +) {} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/UserInfo.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/UserInfo.java new file mode 100644 index 0000000..3cf54b7 --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/UserInfo.java @@ -0,0 +1,9 @@ +package com.shipflow.orderservice.infrastructure.client.dto; + +import java.util.UUID; + +public record UserInfo( + UUID userId, + String ordererName, + UUID receiverCompanyId +) {} diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java index 189a435..069e0bf 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java @@ -33,8 +33,8 @@ public ResponseEntity createOrder( @Valid @RequestBody CreateOrderRequest request, HttpServletRequest httpRequest ) { - UUID requesterId = userContext.getUserId(httpRequest); - OrderResult result = orderCommandService.createOrder(request.toCommand(), requesterId); + UUID ordererId = userContext.getUserId(httpRequest); + OrderResult result = orderCommandService.createOrder(request, ordererId); return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(result)); } diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/CreateOrderRequest.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/CreateOrderRequest.java index 8088727..e7935f1 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/CreateOrderRequest.java +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/dto/CreateOrderRequest.java @@ -1,6 +1,5 @@ package com.shipflow.orderservice.presentation.dto; -import com.shipflow.orderservice.application.dto.CreateOrderCommand; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; @@ -8,22 +7,9 @@ import java.util.UUID; public record CreateOrderRequest( - @NotNull UUID ordererId, @NotNull UUID productId, - @NotNull UUID supplierCompanyId, - @NotNull UUID receiverCompanyId, - @NotNull UUID departureHubId, - @NotNull UUID arrivalHubId, @Min(1) int quantity, @NotNull LocalDateTime requestDeadline, String requestNote ) { - public CreateOrderCommand toCommand() { - return new CreateOrderCommand( - ordererId, productId, - supplierCompanyId, receiverCompanyId, - departureHubId, arrivalHubId, - quantity, requestDeadline, requestNote - ); - } } From 04e28325917a8085c3af828242782a755835b3cb Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 13:00:38 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix(order)=20:=20orderIntegrationTest=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keycloak/shipflow-export.json | 4211 ++++++++++------- .../controller/OrderInternalController.java | 2 +- .../application/OrderCommandServiceTest.java | 15 +- .../orderservice/fixture/OrderFixture.java | 5 +- .../integration/OrderIntegrationTest.java | 19 +- .../presentation/OrderControllerTest.java | 18 +- .../src/test/resources/test-schema.sql | 6 + 7 files changed, 2508 insertions(+), 1768 deletions(-) diff --git a/keycloak/shipflow-export.json b/keycloak/shipflow-export.json index 0f59194..9441476 100644 --- a/keycloak/shipflow-export.json +++ b/keycloak/shipflow-export.json @@ -1,1799 +1,2520 @@ { - "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": { + } }, - "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": "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": { + } }, - "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" ] + { + "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": { + } }, - "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": "c5fa6724-d145-484d-8f33-dafa65b13b74", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "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": "d5324b44-3656-4978-bb13-96915cdf60ff", + "name": "impersonation", + "description": "${role_impersonation}", + "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": "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": { + } }, - "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": "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" + "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": { + }, - "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" ] - } ] + "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": "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" : "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": "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" : "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" + ], + "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" : "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" + ], + "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" : "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" + ] + }, + "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" : "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": "8d580f89-f9cd-4259-ab24-affe163ff5e7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" } - }, { - "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" + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": { + } - } ] - }, { - "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" + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": { + } - } ] - }, { - "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" + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": false, + "defaultAction": false, + "priority": 30, + "config": { + } - } ] - }, { - "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" + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": { + } - }, { - "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" + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": { + } - } ] - }, { - "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" + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": { + } - } ] - }, { - "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" + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": { + } - } ] - }, { - "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" + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": { + } - }, { - "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" + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": { + } - } ] - }, { - "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" + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": { + } - }, { - "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" + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": { + } - } ] - } ], - "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" + ], + "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/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderInternalController.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderInternalController.java index a1d2414..9ca0c22 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderInternalController.java +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderInternalController.java @@ -32,7 +32,7 @@ public ResponseEntity prepareOrder( HttpServletRequest httpRequest ) { UUID requesterId = userContext.getUserId(httpRequest); - OrderResult result = orderCommandService.createOrder(request.toCommand(), requesterId); + OrderResult result = orderCommandService.createOrder(request, requesterId); return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(result)); } diff --git a/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java b/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java index 659a55e..609be9a 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java @@ -7,11 +7,13 @@ import com.shipflow.orderservice.application.dto.OrderResult; import com.shipflow.orderservice.application.dto.UpdateOrderCommand; import com.shipflow.orderservice.application.service.OrderCommandService; +import com.shipflow.orderservice.application.service.OrderFetchService; import com.shipflow.orderservice.domain.exception.OrderNotFoundException; import com.shipflow.orderservice.domain.model.Order; import com.shipflow.orderservice.domain.model.OrderStatus; import com.shipflow.orderservice.domain.repository.OrderRepository; import com.shipflow.orderservice.fixture.OrderFixture; +import com.shipflow.orderservice.presentation.dto.CreateOrderRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,6 +37,7 @@ class OrderCommandServiceTest { @Mock OrderRepository orderRepository; @Mock EventPublisher eventPublisher; @Mock ApplicationEventPublisher domainEventPublisher; + @Mock OrderFetchService orderFetchService; @InjectMocks OrderCommandService orderCommandService; private final UUID orderId = OrderFixture.ORDER_ID; @@ -46,16 +50,19 @@ class OrderCommandServiceTest { @Test void createOrder_성공_저장후이벤트발행() { Order order = OrderFixture.order(orderId); - when(orderRepository.save(any(Order.class))).thenReturn(order); // 가짜 객체 save 가 실행되면 실제로 실행하지 않고 order를 return 받는 과정 CreateOrderCommand cmd = new CreateOrderCommand( - OrderFixture.USER_ID, OrderFixture.PRODUCT_ID, - OrderFixture.SUPPLIER_ID, OrderFixture.RECEIVER_ID, + OrderFixture.USER_ID, "주문자명", OrderFixture.PRODUCT_ID, "상품명", + OrderFixture.SUPPLIER_ID, "공급사명", + OrderFixture.RECEIVER_ID, "수신사명", OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID, 10, OrderFixture.DEADLINE, "테스트 메모" ); + when(orderFetchService.fetchAndBuild(any(), any(), anyInt(), any(), any())).thenReturn(cmd); + when(orderRepository.save(any(Order.class))).thenReturn(order); - OrderResult result = orderCommandService.createOrder(cmd, userId); + CreateOrderRequest request = OrderFixture.createRequest(); + OrderResult result = orderCommandService.createOrder(request, userId); assertThat(result.status()).isEqualTo(OrderStatus.CREATING); verify(orderRepository).save(any(Order.class)); diff --git a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java index 1754f71..9af4352 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java +++ b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java @@ -121,10 +121,7 @@ public static OrderReadModel createdOrderReadModel(UUID orderId) { public static CreateOrderRequest createRequest() { return new CreateOrderRequest( - USER_ID, PRODUCT_ID, - SUPPLIER_ID, RECEIVER_ID, - DEP_HUB_ID, ARR_HUB_ID, - 10, DEADLINE, "테스트 메모" + PRODUCT_ID, 10, DEADLINE, "테스트 메모" ); } diff --git a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java index e4343db..8681202 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java @@ -1,6 +1,8 @@ package com.shipflow.orderservice.integration; import com.fasterxml.jackson.databind.ObjectMapper; +import com.shipflow.orderservice.application.dto.CreateOrderCommand; +import com.shipflow.orderservice.application.service.OrderFetchService; import com.shipflow.orderservice.fixture.OrderFixture; import com.shipflow.orderservice.presentation.dto.OrderResponse; import org.junit.jupiter.api.BeforeEach; @@ -8,12 +10,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -22,11 +28,22 @@ class OrderIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Autowired JdbcTemplate jdbcTemplate; + @MockitoBean OrderFetchService orderFetchService; @BeforeEach - void cleanUp() { + void setUp() { jdbcTemplate.execute("DELETE FROM orders.p_order_read_models"); jdbcTemplate.execute("DELETE FROM orders.p_orders"); + + when(orderFetchService.fetchAndBuild(any(), any(), anyInt(), any(), any())) + .thenReturn(new CreateOrderCommand( + OrderFixture.USER_ID, "주문자명", + OrderFixture.PRODUCT_ID, "상품명", + OrderFixture.SUPPLIER_ID, "공급사명", + OrderFixture.RECEIVER_ID, "수신사명", + OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID, + 10, OrderFixture.DEADLINE, "테스트 메모" + )); } @Test diff --git a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java index 0eeaf94..c58e466 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java @@ -67,13 +67,10 @@ class OrderControllerTest { @Test void createOrder_필수필드누락_400반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); + // productId 누락 String invalidBody = """ - {"productId":"%s","supplierCompanyId":"%s","receiverCompanyId":"%s", - "departureHubId":"%s","arrivalHubId":"%s","quantity":1, - "requestDeadline":"2026-12-31T23:59:00"} - """.formatted( - OrderFixture.PRODUCT_ID, OrderFixture.SUPPLIER_ID, OrderFixture.RECEIVER_ID, - OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID); + {"quantity":1,"requestDeadline":"2026-12-31T23:59:00"} + """; mockMvc.perform(post("/api/orders") .header("X-User-Id", userId.toString()) @@ -86,13 +83,8 @@ class OrderControllerTest { void createOrder_수량0이하_400반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); String invalidBody = """ - {"ordererId":"%s","productId":"%s","supplierCompanyId":"%s", - "receiverCompanyId":"%s","departureHubId":"%s","arrivalHubId":"%s", - "quantity":0,"requestDeadline":"2026-12-31T23:59:00"} - """.formatted( - OrderFixture.USER_ID, OrderFixture.PRODUCT_ID, - OrderFixture.SUPPLIER_ID, OrderFixture.RECEIVER_ID, - OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID); + {"productId":"%s","quantity":0,"requestDeadline":"2026-12-31T23:59:00"} + """.formatted(OrderFixture.PRODUCT_ID); mockMvc.perform(post("/api/orders") .header("X-User-Id", userId.toString()) diff --git a/order-service/src/test/resources/test-schema.sql b/order-service/src/test/resources/test-schema.sql index 786a765..3ba7eae 100644 --- a/order-service/src/test/resources/test-schema.sql +++ b/order-service/src/test/resources/test-schema.sql @@ -1,5 +1,11 @@ CREATE SCHEMA IF NOT EXISTS orders; +CREATE TABLE IF NOT EXISTS orders.processed_saga_events ( + event_id VARCHAR(36) PRIMARY KEY, + event_type VARCHAR(100) NOT NULL, + processed_at TIMESTAMP(6) NOT NULL +); + CREATE TABLE IF NOT EXISTS orders.p_orders ( id UUID PRIMARY KEY, orderer_id UUID NOT NULL, From 6c3a285eca40602aa361fa24a86b39a5352f8e50 Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 15:28:12 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix(order)=20:=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EB=B0=B0?= =?UTF-8?q?=EC=86=A1=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD(requestNote)=20fi?= =?UTF-8?q?eld=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orderservice/application/service/OrderCommandService.java | 3 ++- .../messaging/event/publish/OrderCreatedEvent.java | 4 +++- order-service/src/main/resources/application.yaml | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java index 7422384..c88250d 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java @@ -85,7 +85,8 @@ public void confirmCreation(UUID orderId, String productName) { saved.getQuantity().getValue(), saved.getHubInfo().getDepartureHubId(), saved.getHubInfo().getArrivalHubId(), - saved.getRequestDeadline() + saved.getRequestDeadline(), + saved.getRequestNote() ) ); } diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java index 8b890c7..b11e2fe 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java @@ -21,11 +21,12 @@ public class OrderCreatedEvent extends SagaEvent { private UUID departureHubId; private UUID arrivalHubId; private LocalDateTime requestDeadline; + private String requestNote; public OrderCreatedEvent(UUID orderId, UUID supplierCompanyId, UUID receiverCompanyId, UUID productId, int quantity, UUID departureHubId, UUID arrivalHubId, - LocalDateTime requestDeadline) { + LocalDateTime requestDeadline, String requestNote) { super(EVENT_TYPE); this.orderId = orderId; this.supplierCompanyId = supplierCompanyId; @@ -35,5 +36,6 @@ public OrderCreatedEvent(UUID orderId, UUID supplierCompanyId, UUID receiverComp this.departureHubId = departureHubId; this.arrivalHubId = arrivalHubId; this.requestDeadline = requestDeadline; + this.requestNote = requestNote; } } diff --git a/order-service/src/main/resources/application.yaml b/order-service/src/main/resources/application.yaml index e0311a9..de4cc29 100644 --- a/order-service/src/main/resources/application.yaml +++ b/order-service/src/main/resources/application.yaml @@ -1,6 +1,6 @@ spring: application: - name: orderservice + name: order-service # 데이터베이스 설정 datasource: @@ -50,7 +50,7 @@ server: eureka: client: service-url: - defaultZone: http://discoveryserver:8761/eureka/ + defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/} registry-fetch-interval-seconds: 5 instance: prefer-ip-address: true From d742604d5c215dde57a9e6b714f8b6b0b64d5c58 Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 15:37:25 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix(order)=20:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90ID(ordererId)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orderservice/application/service/OrderCommandService.java | 1 + .../messaging/event/publish/OrderCreatedEvent.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java index c88250d..99a4cc0 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java @@ -79,6 +79,7 @@ public void confirmCreation(UUID orderId, String productName) { rabbitPublisher.publish( new OrderCreatedEvent( saved.getId(), + saved.getOrdererId(), saved.getCompanyInfo().getSupplierCompanyId(), saved.getCompanyInfo().getReceiverCompanyId(), saved.getProductId(), diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java index b11e2fe..3d7a327 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java @@ -14,6 +14,7 @@ public class OrderCreatedEvent extends SagaEvent { private static final String EVENT_TYPE = "order.created"; private UUID orderId; + private UUID ordererId; private UUID supplierCompanyId; private UUID receiverCompanyId; private UUID productId; @@ -23,12 +24,13 @@ public class OrderCreatedEvent extends SagaEvent { private LocalDateTime requestDeadline; private String requestNote; - public OrderCreatedEvent(UUID orderId, UUID supplierCompanyId, UUID receiverCompanyId, + public OrderCreatedEvent(UUID orderId, UUID ordererId, UUID supplierCompanyId, UUID receiverCompanyId, UUID productId, int quantity, UUID departureHubId, UUID arrivalHubId, LocalDateTime requestDeadline, String requestNote) { super(EVENT_TYPE); this.orderId = orderId; + this.ordererId = ordererId; this.supplierCompanyId = supplierCompanyId; this.receiverCompanyId = receiverCompanyId; this.productId = productId; From 02ab6d64b07fc52097c1e06f434d95a87907bce6 Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 17:58:34 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor(order)=20:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?API=20=EC=9D=B8=EA=B0=80=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/OrderSearchCondition.java | 7 +- .../service/OrderQueryService.java | 10 ++ .../domain/exception/OrderErrorCode.java | 1 + .../exception/UnauthorizedException.java | 10 ++ .../orderservice/domain/model/UserRole.java | 31 +++++ .../infrastructure/web/UserContext.java | 5 +- .../controller/OrderController.java | 32 ++++-- .../integration/OrderIntegrationTest.java | 21 +++- .../presentation/OrderControllerTest.java | 108 ++++++++++++++++-- 9 files changed, 199 insertions(+), 26 deletions(-) create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/exception/UnauthorizedException.java create mode 100644 order-service/src/main/java/com/shipflow/orderservice/domain/model/UserRole.java diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java index 7be2e70..56286b7 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderSearchCondition.java @@ -13,4 +13,9 @@ public record OrderSearchCondition( UUID receiverCompanyId, LocalDateTime createdFrom, LocalDateTime createdTo -) {} +) { + public OrderSearchCondition withOrdererId(UUID forcedOrdererId) { + return new OrderSearchCondition(status, forcedOrdererId, productId, + supplierCompanyId, receiverCompanyId, createdFrom, createdTo); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java index d6b8359..0c07c24 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderQueryService.java @@ -3,8 +3,10 @@ import com.shipflow.orderservice.application.dto.OrderResult; import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.domain.exception.OrderNotFoundException; +import com.shipflow.orderservice.domain.exception.UnauthorizedException; import com.shipflow.orderservice.domain.model.Order; import com.shipflow.orderservice.domain.model.OrderReadModel; +import com.shipflow.orderservice.domain.model.UserRole; import com.shipflow.orderservice.domain.repository.OrderReadModelRepository; import com.shipflow.orderservice.domain.repository.OrderRepository; import lombok.RequiredArgsConstructor; @@ -38,6 +40,14 @@ public OrderResult getOrder(UUID orderId) { return OrderResult.from(order); } + public OrderResult getOrder(UUID orderId, UUID requesterId, UserRole role) { + OrderResult result = getOrder(orderId); + if (role.isRestrictedToOwnOrders() && !result.ordererId().equals(requesterId)) { + throw new UnauthorizedException(); + } + return result; + } + /** * 전체 주문 목록을 조회합니다. * 데이터 양이 적거나, 시스템 내부의 배치 작업/동기화 작업 시 사용합니다. diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java index 994a000..d5ef6ed 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/OrderErrorCode.java @@ -5,6 +5,7 @@ public enum OrderErrorCode implements ErrorCode { + UNAUTHORIZED("UNAUTHORIZED", HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), ORDER_NOT_FOUND("ORDER_NOT_FOUND", HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), INVALID_ORDER_STATE("INVALID_ORDER_STATE", HttpStatus.CONFLICT, "유효하지 않은 주문 상태입니다."), PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UnauthorizedException.java b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UnauthorizedException.java new file mode 100644 index 0000000..03348bf --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/exception/UnauthorizedException.java @@ -0,0 +1,10 @@ +package com.shipflow.orderservice.domain.exception; + +import com.shipflow.common.exception.BusinessException; + +public class UnauthorizedException extends BusinessException { + + public UnauthorizedException() { + super(OrderErrorCode.UNAUTHORIZED); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/model/UserRole.java b/order-service/src/main/java/com/shipflow/orderservice/domain/model/UserRole.java new file mode 100644 index 0000000..264be2d --- /dev/null +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/model/UserRole.java @@ -0,0 +1,31 @@ +package com.shipflow.orderservice.domain.model; + +import com.shipflow.orderservice.domain.exception.UnauthorizedException; + +public enum UserRole { + + MASTER, HUB_MANAGER, SHIPMENT_MANAGER, COMPANY_MANAGER; + + public static UserRole from(String value) { + try { + return valueOf(value); + } catch (IllegalArgumentException e) { + throw new UnauthorizedException(); + } + } + + /** 수정/삭제/취소 권한 (MASTER, HUB_MANAGER) */ + public boolean canManageOrder() { + return this == MASTER || this == HUB_MANAGER; + } + + /** 본인 주문만 조회 가능한 역할 (SHIPMENT_MANAGER, COMPANY_MANAGER) */ + public boolean isRestrictedToOwnOrders() { + return this == COMPANY_MANAGER || this == SHIPMENT_MANAGER; + } + + /** 수정/삭제/취소 권한 검증 — 권한 없으면 UnauthorizedException */ + public void requireManageOrder() { + if (!canManageOrder()) throw new UnauthorizedException(); + } +} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/web/UserContext.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/web/UserContext.java index aeb0a83..ba34b7a 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/web/UserContext.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/web/UserContext.java @@ -1,5 +1,6 @@ package com.shipflow.orderservice.infrastructure.web; +import com.shipflow.orderservice.domain.model.UserRole; import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; @@ -20,11 +21,11 @@ public UUID getUserId(HttpServletRequest request) { return UUID.fromString(userId); } - public String getUserRole(HttpServletRequest request) { + public UserRole getUserRole(HttpServletRequest request) { String role = request.getHeader("X-User-Role"); if (role == null || role.isBlank()) { throw new IllegalArgumentException("X-User-Role 헤더가 없습니다."); } - return role; + return UserRole.from(role); } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java index 069e0bf..8e3c0c7 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java +++ b/order-service/src/main/java/com/shipflow/orderservice/presentation/controller/OrderController.java @@ -1,9 +1,11 @@ package com.shipflow.orderservice.presentation.controller; import com.shipflow.orderservice.application.dto.OrderResult; +import com.shipflow.orderservice.application.dto.OrderSearchCondition; import com.shipflow.orderservice.application.service.OrderCommandService; import com.shipflow.orderservice.application.service.OrderQueryService; import com.shipflow.orderservice.domain.model.OrderReadModel; +import com.shipflow.orderservice.domain.model.UserRole; import com.shipflow.orderservice.infrastructure.web.UserContext; import com.shipflow.orderservice.presentation.dto.*; import jakarta.servlet.http.HttpServletRequest; @@ -39,19 +41,28 @@ public ResponseEntity createOrder( } @GetMapping("/{orderId}") - public ResponseEntity getOrder(@PathVariable UUID orderId) { - return ResponseEntity.ok(OrderResponse.from(orderQueryService.getOrder(orderId))); + public ResponseEntity getOrder( + @PathVariable UUID orderId, + HttpServletRequest httpRequest + ) { + UUID requesterId = userContext.getUserId(httpRequest); + UserRole role = userContext.getUserRole(httpRequest); + return ResponseEntity.ok(OrderResponse.from(orderQueryService.getOrder(orderId, requesterId, role))); } @GetMapping public ResponseEntity> getOrders( @ModelAttribute OrderSearchRequest searchRequest, @PageableDefault(size = 10, page = 0, sort = "createdAt", - direction = Sort.Direction.DESC) Pageable pageable + direction = Sort.Direction.DESC) Pageable pageable, + HttpServletRequest httpRequest ) { - Slice result = orderQueryService.searchOrders( - searchRequest.toCondition(), pageable - ); + UUID requesterId = userContext.getUserId(httpRequest); + UserRole role = userContext.getUserRole(httpRequest); + OrderSearchCondition condition = role.isRestrictedToOwnOrders() + ? searchRequest.toCondition().withOrdererId(requesterId) + : searchRequest.toCondition(); + Slice result = orderQueryService.searchOrders(condition, pageable); return ResponseEntity.ok(result.map(OrderReadModelResponse::from)); } @@ -62,6 +73,8 @@ public ResponseEntity updateOrder( HttpServletRequest httpRequest ) { UUID requesterId = userContext.getUserId(httpRequest); + UserRole role = userContext.getUserRole(httpRequest); + role.requireManageOrder(); OrderResult result = orderCommandService.updateOrder(orderId, request.toCommand(), requesterId); return ResponseEntity.ok(OrderResponse.from(result)); } @@ -69,8 +82,11 @@ public ResponseEntity updateOrder( @PostMapping("/{orderId}/cancel") public ResponseEntity cancelOrder( @PathVariable UUID orderId, - @Valid @RequestBody CancelOrderRequest request + @Valid @RequestBody CancelOrderRequest request, + HttpServletRequest httpRequest ) { + UserRole role = userContext.getUserRole(httpRequest); + role.requireManageOrder(); orderCommandService.cancelOrder(orderId, request.toCommand()); return ResponseEntity.ok().build(); } @@ -81,6 +97,8 @@ public ResponseEntity deleteOrder( HttpServletRequest httpRequest ) { UUID requesterId = userContext.getUserId(httpRequest); + UserRole role = userContext.getUserRole(httpRequest); + role.requireManageOrder(); orderCommandService.deleteOrder(orderId, requesterId); return ResponseEntity.noContent().build(); } diff --git a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java index 8681202..07b835c 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java @@ -53,6 +53,7 @@ void setUp() { // 1. 주문 생성 MvcResult createResult = mockMvc.perform(post("/api/orders") .header("X-User-Id", userId.toString()) + .header("X-User-Role", "COMPANY_MANAGER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.createRequest()))) .andExpect(status().isCreated()) @@ -69,8 +70,10 @@ void setUp() { "SELECT COUNT(*) FROM orders.p_orders WHERE id = ?::uuid", Integer.class, orderId.toString() )).isEqualTo(1); - // 4. 단건 조회 - mockMvc.perform(get("/api/orders/{id}", orderId)) + // 4. 단건 조회 (MASTER - 전체 접근 가능) + mockMvc.perform(get("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(orderId.toString())) .andExpect(jsonPath("$.quantity").value(10)); @@ -83,6 +86,7 @@ void setUp() { // 1. 주문 생성 MvcResult createResult = mockMvc.perform(post("/api/orders") .header("X-User-Id", userId.toString()) + .header("X-User-Role", "COMPANY_MANAGER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.createRequest()))) .andExpect(status().isCreated()) @@ -92,14 +96,17 @@ void setUp() { createResult.getResponse().getContentAsString(), OrderResponse.class); UUID orderId = created.id(); - // 2. 취소 + // 2. 취소 (MASTER 권한) mockMvc.perform(post("/api/orders/{id}/cancel", orderId) + .header("X-User-Role", "MASTER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.cancelRequest()))) .andExpect(status().isOk()); - // 3. 취소 상태 확인 - mockMvc.perform(get("/api/orders/{id}", orderId)) + // 3. 취소 상태 확인 (MASTER 권한) + mockMvc.perform(get("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("CANCELED")) .andExpect(jsonPath("$.cancelReason").value("재고 부족")); @@ -109,7 +116,9 @@ void setUp() { void 없는주문조회_404반환() throws Exception { UUID nonExistentId = UUID.randomUUID(); - mockMvc.perform(get("/api/orders/{id}", nonExistentId)) + mockMvc.perform(get("/api/orders/{id}", nonExistentId) + .header("X-User-Id", UUID.randomUUID().toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isNotFound()); } } diff --git a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java index c58e466..0ea79b1 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/presentation/OrderControllerTest.java @@ -7,6 +7,7 @@ import com.shipflow.orderservice.domain.exception.OrderNotFoundException; import com.shipflow.orderservice.domain.model.OrderReadModel; import com.shipflow.orderservice.domain.model.OrderStatus; +import com.shipflow.orderservice.domain.model.UserRole; import com.shipflow.orderservice.fixture.OrderFixture; import com.shipflow.orderservice.infrastructure.web.UserContext; import com.shipflow.orderservice.presentation.controller.OrderController; @@ -99,10 +100,14 @@ class OrderControllerTest { @Test void getOrder_성공_200반환() throws Exception { - when(orderQueryService.getOrder(orderId)) + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); + when(orderQueryService.getOrder(eq(orderId), eq(userId), eq(UserRole.MASTER))) .thenReturn(OrderFixture.orderResult(orderId)); - mockMvc.perform(get("/api/orders/{id}", orderId)) + mockMvc.perform(get("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(orderId.toString())) .andExpect(jsonPath("$.status").value("CREATING")); @@ -110,10 +115,14 @@ class OrderControllerTest { @Test void getOrder_없는ID_404반환() throws Exception { - when(orderQueryService.getOrder(orderId)) + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); + when(orderQueryService.getOrder(eq(orderId), any(), any())) .thenThrow(new OrderNotFoundException(orderId)); - mockMvc.perform(get("/api/orders/{id}", orderId)) + mockMvc.perform(get("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isNotFound()); } @@ -123,6 +132,8 @@ class OrderControllerTest { @Test void getOrders_기본요청_200반환() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); OrderReadModel model = OrderFixture.orderReadModel(orderId); Slice slice = new SliceImpl<>( List.of(model), @@ -131,7 +142,9 @@ class OrderControllerTest { ); when(orderQueryService.searchOrders(any(), any())).thenReturn(slice); - mockMvc.perform(get("/api/orders")) + mockMvc.perform(get("/api/orders") + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isOk()) .andExpect(jsonPath("$.content[0].orderId").value(orderId.toString())) .andExpect(jsonPath("$.content[0].orderStatus").value("CREATING")); @@ -139,25 +152,53 @@ class OrderControllerTest { @Test void getOrders_상태필터CREATED_파라미터전달됨() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); Slice slice = new SliceImpl<>(List.of(), PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")), false); ArgumentCaptor conditionCaptor = ArgumentCaptor.forClass(OrderSearchCondition.class); when(orderQueryService.searchOrders(conditionCaptor.capture(), any())).thenReturn(slice); - mockMvc.perform(get("/api/orders").param("status", "CREATED")) + mockMvc.perform(get("/api/orders") + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER") + .param("status", "CREATED")) .andExpect(status().isOk()); assertThat(conditionCaptor.getValue().status()).isEqualTo(OrderStatus.CREATED); } + @Test + void getOrders_COMPANY_MANAGER_본인주문만조회() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.COMPANY_MANAGER); + Slice slice = new SliceImpl<>(List.of(), + PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")), false); + ArgumentCaptor conditionCaptor = + ArgumentCaptor.forClass(OrderSearchCondition.class); + when(orderQueryService.searchOrders(conditionCaptor.capture(), any())).thenReturn(slice); + + mockMvc.perform(get("/api/orders") + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "COMPANY_MANAGER")) + .andExpect(status().isOk()); + + assertThat(conditionCaptor.getValue().ordererId()).isEqualTo(userId); + } + @Test void getOrders_페이지네이션_파라미터전달됨() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); Slice slice = new SliceImpl<>(List.of(), PageRequest.of(1, 30, Sort.by(Sort.Direction.DESC, "createdAt")), false); when(orderQueryService.searchOrders(any(), any())).thenReturn(slice); - mockMvc.perform(get("/api/orders").param("page", "1").param("size", "30")) + mockMvc.perform(get("/api/orders") + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER") + .param("page", "1").param("size", "30")) .andExpect(status().isOk()); } @@ -168,25 +209,42 @@ class OrderControllerTest { @Test void updateOrder_성공_200반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); when(orderCommandService.updateOrder(eq(orderId), any(), eq(userId))) .thenReturn(OrderFixture.orderResult(orderId)); mockMvc.perform(patch("/api/orders/{id}", orderId) .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.updateRequest()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(orderId.toString())); } + @Test + void updateOrder_권한없음_403반환() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.COMPANY_MANAGER); + + mockMvc.perform(patch("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "COMPANY_MANAGER") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(OrderFixture.updateRequest()))) + .andExpect(status().isForbidden()); + } + @Test void updateOrder_없는ID_404반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); when(orderCommandService.updateOrder(eq(orderId), any(), any())) .thenThrow(new OrderNotFoundException(orderId)); mockMvc.perform(patch("/api/orders/{id}", orderId) .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.updateRequest()))) .andExpect(status().isNotFound()); @@ -198,20 +256,35 @@ class OrderControllerTest { @Test void cancelOrder_성공_200반환() throws Exception { + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); doNothing().when(orderCommandService).cancelOrder(eq(orderId), any()); mockMvc.perform(post("/api/orders/{id}/cancel", orderId) + .header("X-User-Role", "MASTER") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(OrderFixture.cancelRequest()))) .andExpect(status().isOk()); } + @Test + void cancelOrder_권한없음_403반환() throws Exception { + when(userContext.getUserRole(any())).thenReturn(UserRole.COMPANY_MANAGER); + + mockMvc.perform(post("/api/orders/{id}/cancel", orderId) + .header("X-User-Role", "COMPANY_MANAGER") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(OrderFixture.cancelRequest()))) + .andExpect(status().isForbidden()); + } + @Test void cancelOrder_사유없음_400반환() throws Exception { + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); + mockMvc.perform(post("/api/orders/{id}/cancel", orderId) + .header("X-User-Role", "MASTER") .contentType(MediaType.APPLICATION_JSON) .content("{\"reason\": \"\"}")) - .andExpect(status().isBadRequest()); } @@ -222,21 +295,36 @@ class OrderControllerTest { @Test void deleteOrder_성공_204반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); doNothing().when(orderCommandService).deleteOrder(eq(orderId), eq(userId)); mockMvc.perform(delete("/api/orders/{id}", orderId) - .header("X-User-Id", userId.toString())) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isNoContent()); } + @Test + void deleteOrder_권한없음_403반환() throws Exception { + when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.SHIPMENT_MANAGER); + + mockMvc.perform(delete("/api/orders/{id}", orderId) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "SHIPMENT_MANAGER")) + .andExpect(status().isForbidden()); + } + @Test void deleteOrder_없는ID_404반환() throws Exception { when(userContext.getUserId(any())).thenReturn(userId); + when(userContext.getUserRole(any())).thenReturn(UserRole.MASTER); doThrow(new OrderNotFoundException(orderId)) .when(orderCommandService).deleteOrder(eq(orderId), any()); mockMvc.perform(delete("/api/orders/{id}", orderId) - .header("X-User-Id", userId.toString())) + .header("X-User-Id", userId.toString()) + .header("X-User-Role", "MASTER")) .andExpect(status().isNotFound()); } } From 4161d402ddd58d21343a955d9f3085c8e26529ce Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 19:44:53 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix(order)=20:=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20event=EC=97=90=20=EB=B0=B0=EC=86=A1?= =?UTF-8?q?=EC=A7=80=20=EC=A3=BC=EC=86=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orderservice/application/dto/CreateOrderCommand.java | 3 ++- .../shipflow/orderservice/application/dto/OrderResult.java | 2 ++ .../application/service/OrderCommandService.java | 4 +++- .../orderservice/application/service/OrderFetchService.java | 3 ++- .../java/com/shipflow/orderservice/domain/model/Order.java | 6 +++++- .../infrastructure/client/dto/ReceiverCompanyInfo.java | 3 ++- .../messaging/event/publish/OrderCreatedEvent.java | 4 +++- .../infrastructure/persistence/OrderJpaEntity.java | 5 ++++- .../orderservice/application/OrderCommandServiceTest.java | 2 +- .../com/shipflow/orderservice/fixture/OrderFixture.java | 6 +++--- .../orderservice/integration/OrderIntegrationTest.java | 2 +- order-service/src/test/resources/test-schema.sql | 1 + 12 files changed, 29 insertions(+), 12 deletions(-) diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java b/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java index fd09fe9..00eb248 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/dto/CreateOrderCommand.java @@ -16,6 +16,7 @@ public record CreateOrderCommand( UUID arrivalHubId, int quantity, LocalDateTime requestDeadline, - String requestNote + String requestNote, + String deliveryAddress ) { } diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderResult.java b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderResult.java index 44c427e..7d2c6a0 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderResult.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/dto/OrderResult.java @@ -20,6 +20,7 @@ public record OrderResult( String cancelReason, LocalDateTime requestDeadline, String requestNote, + String deliveryAddress, UUID createdBy, LocalDateTime createdAt, UUID updatedBy, @@ -40,6 +41,7 @@ public static OrderResult from(Order order) { order.getCancelReason(), order.getRequestDeadline(), order.getRequestNote(), + order.getDeliveryAddress(), order.getCreatedBy(), order.getCreatedAt(), order.getUpdatedBy(), diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java index 99a4cc0..b2a8545 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderCommandService.java @@ -46,6 +46,7 @@ public OrderResult createOrder(CreateOrderRequest request, UUID ordererId) { new Quantity(cmd.quantity()), cmd.requestDeadline(), cmd.requestNote(), + cmd.deliveryAddress(), ordererId ); Order saved = orderRepository.save(order); @@ -87,7 +88,8 @@ public void confirmCreation(UUID orderId, String productName) { saved.getHubInfo().getDepartureHubId(), saved.getHubInfo().getArrivalHubId(), saved.getRequestDeadline(), - saved.getRequestNote() + saved.getRequestNote(), + saved.getDeliveryAddress() ) ); } diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java index c722f50..7285e89 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java @@ -51,7 +51,8 @@ public CreateOrderCommand fetchAndBuild(UUID ordererId, UUID productId, company.hubId(), quantity, deadline, - note + note, + company.address() ); } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/domain/model/Order.java b/order-service/src/main/java/com/shipflow/orderservice/domain/model/Order.java index f400426..ba3af03 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/domain/model/Order.java +++ b/order-service/src/main/java/com/shipflow/orderservice/domain/model/Order.java @@ -23,6 +23,7 @@ public class Order { // 도메인 모델 private String cancelReason; private LocalDateTime requestDeadline; private String requestNote; + private String deliveryAddress; private UUID createdBy; private LocalDateTime createdAt; private UUID updatedBy; @@ -41,6 +42,7 @@ public static Order create( Quantity quantity, LocalDateTime requestDeadline, String requestNote, + String deliveryAddress, UUID createdBy ) { Order order = new Order(); @@ -52,6 +54,7 @@ public static Order create( order.quantity = quantity; order.requestDeadline = requestDeadline; order.requestNote = requestNote; + order.deliveryAddress = deliveryAddress; order.createdBy = createdBy; order.status = OrderStatus.CREATING; order.createdAt = LocalDateTime.now(); @@ -111,7 +114,7 @@ public static Order reconstruct( UUID id, UUID ordererId, UUID productId, UUID shipmentId, CompanyInfo companyInfo, HubInfo hubInfo, Quantity quantity, OrderStatus status, String cancelReason, - LocalDateTime requestDeadline, String requestNote, + LocalDateTime requestDeadline, String requestNote, String deliveryAddress, UUID createdBy, LocalDateTime createdAt, UUID updatedBy, LocalDateTime updatedAt, UUID deletedBy, LocalDateTime deletedAt @@ -128,6 +131,7 @@ public static Order reconstruct( order.cancelReason = cancelReason; order.requestDeadline = requestDeadline; order.requestNote = requestNote; + order.deliveryAddress = deliveryAddress; order.createdBy = createdBy; order.createdAt = createdAt; order.updatedBy = updatedBy; diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java index 1cfccb2..d683583 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/dto/ReceiverCompanyInfo.java @@ -5,5 +5,6 @@ public record ReceiverCompanyInfo( UUID companyId, String companyName, - UUID hubId + UUID hubId, + String address ) {} diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java index 3d7a327..fbdea1a 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCreatedEvent.java @@ -23,11 +23,12 @@ public class OrderCreatedEvent extends SagaEvent { private UUID arrivalHubId; private LocalDateTime requestDeadline; private String requestNote; + private String deliveryAddress; public OrderCreatedEvent(UUID orderId, UUID ordererId, UUID supplierCompanyId, UUID receiverCompanyId, UUID productId, int quantity, UUID departureHubId, UUID arrivalHubId, - LocalDateTime requestDeadline, String requestNote) { + LocalDateTime requestDeadline, String requestNote, String deliveryAddress) { super(EVENT_TYPE); this.orderId = orderId; this.ordererId = ordererId; @@ -39,5 +40,6 @@ public OrderCreatedEvent(UUID orderId, UUID ordererId, UUID supplierCompanyId, U this.arrivalHubId = arrivalHubId; this.requestDeadline = requestDeadline; this.requestNote = requestNote; + this.deliveryAddress = deliveryAddress; } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderJpaEntity.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderJpaEntity.java index df765c3..5ede4df 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderJpaEntity.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/persistence/OrderJpaEntity.java @@ -56,6 +56,8 @@ public class OrderJpaEntity extends BaseEntity { // 데이터 모델 private String requestNote; + private String deliveryAddress; + // Order -> OrderJpaEntity (Order 객체를 DB 에 저장할때) public static OrderJpaEntity from(Order order) { OrderJpaEntity entity = new OrderJpaEntity(); @@ -70,6 +72,7 @@ public static OrderJpaEntity from(Order order) { entity.cancelReason = order.getCancelReason(); entity.requestDeadline = order.getRequestDeadline(); entity.requestNote = order.getRequestNote(); + entity.deliveryAddress = order.getDeliveryAddress(); entity.createdBy = order.getCreatedBy(); entity.createdAt = order.getCreatedAt(); entity.updatedBy = order.getUpdatedBy(); @@ -84,7 +87,7 @@ public Order toDomain() { return Order.reconstruct( id, ordererId, productId, shipmentId, companyInfo, hubInfo, quantity, - status, cancelReason, requestDeadline, requestNote, + status, cancelReason, requestDeadline, requestNote, deliveryAddress, createdBy, createdAt, updatedBy, updatedAt, deletedBy, deletedAt ); } diff --git a/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java b/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java index 609be9a..159f132 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/application/OrderCommandServiceTest.java @@ -56,7 +56,7 @@ class OrderCommandServiceTest { OrderFixture.SUPPLIER_ID, "공급사명", OrderFixture.RECEIVER_ID, "수신사명", OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID, - 10, OrderFixture.DEADLINE, "테스트 메모" + 10, OrderFixture.DEADLINE, "테스트 메모", "서울시 강남구 테스트로 1" ); when(orderFetchService.fetchAndBuild(any(), any(), anyInt(), any(), any())).thenReturn(cmd); when(orderRepository.save(any(Order.class))).thenReturn(order); diff --git a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java index 9af4352..fc3f9ea 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java +++ b/order-service/src/test/java/com/shipflow/orderservice/fixture/OrderFixture.java @@ -31,7 +31,7 @@ public static OrderResult orderResult(UUID orderId) { orderId, USER_ID, PRODUCT_ID, null, SUPPLIER_ID, RECEIVER_ID, DEP_HUB_ID, ARR_HUB_ID, 10, OrderStatus.CREATING, null, - DEADLINE, "테스트 메모", + DEADLINE, "테스트 메모", "서울시 강남구 테스트로 1", USER_ID, LocalDateTime.of(2026, 4, 1, 9, 0), null, null ); @@ -45,7 +45,7 @@ public static Order order(UUID orderId) { new HubInfo(DEP_HUB_ID, ARR_HUB_ID), new Quantity(10), OrderStatus.CREATING, null, - DEADLINE, "테스트 메모", + DEADLINE, "테스트 메모", "서울시 강남구 테스트로 1", USER_ID, LocalDateTime.of(2026, 4, 1, 9, 0), null, null, null, null ); @@ -77,7 +77,7 @@ public static Order createdOrder(UUID orderId) { new HubInfo(DEP_HUB_ID, ARR_HUB_ID), new Quantity(10), OrderStatus.CREATED, null, - DEADLINE, "테스트 메모", + DEADLINE, "테스트 메모", "서울시 강남구 테스트로 1", USER_ID, LocalDateTime.of(2026, 4, 1, 9, 0), null, null, null, null ); diff --git a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java index 07b835c..23e3996 100644 --- a/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java +++ b/order-service/src/test/java/com/shipflow/orderservice/integration/OrderIntegrationTest.java @@ -42,7 +42,7 @@ void setUp() { OrderFixture.SUPPLIER_ID, "공급사명", OrderFixture.RECEIVER_ID, "수신사명", OrderFixture.DEP_HUB_ID, OrderFixture.ARR_HUB_ID, - 10, OrderFixture.DEADLINE, "테스트 메모" + 10, OrderFixture.DEADLINE, "테스트 메모", "서울시 강남구 테스트로 1" )); } diff --git a/order-service/src/test/resources/test-schema.sql b/order-service/src/test/resources/test-schema.sql index 3ba7eae..0b8241a 100644 --- a/order-service/src/test/resources/test-schema.sql +++ b/order-service/src/test/resources/test-schema.sql @@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS orders.p_orders ( cancel_reason VARCHAR, request_deadline TIMESTAMP(6), request_note VARCHAR, + delivery_address VARCHAR, created_by UUID, created_at TIMESTAMP(6), updated_by UUID, From 5d94a3280861dcfadd77a172f2e5dff591e6f791 Mon Sep 17 00:00:00 2001 From: t2025-m0135 Date: Mon, 6 Apr 2026 20:13:59 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix(order)=20:=20feign-client=20retry?= =?UTF-8?q?=EC=98=88=EC=99=B8=20unwrapping=20=EC=A0=84=EB=9E=B5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/OrderFetchService.java | 14 +++++++++++++- .../client/adapter/CompanyClientAdapter.java | 2 ++ .../client/adapter/ProductClientAdapter.java | 5 ++++- .../client/adapter/UserClientAdapter.java | 5 ++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java index 7285e89..76c55bc 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java +++ b/order-service/src/main/java/com/shipflow/orderservice/application/service/OrderFetchService.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; @Service @RequiredArgsConstructor @@ -30,7 +31,18 @@ public CreateOrderCommand fetchAndBuild(UUID ordererId, UUID productId, CompletableFuture userFuture = CompletableFuture.supplyAsync( () -> userAdapter.fetch(ordererId)); - CompletableFuture.allOf(productFuture, userFuture).join(); + try { + CompletableFuture.allOf(productFuture, userFuture).join(); + } catch (CompletionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException re) { + throw re; + } + if (cause instanceof Error err) { + throw err; + } + throw e; + } ProductInfo product = productFuture.join(); UserInfo user = userFuture.join(); diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java index 56375dd..cde6f36 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/CompanyClientAdapter.java @@ -27,6 +27,8 @@ public class CompanyClientAdapter { public ReceiverCompanyInfo fetch(UUID companyId) { try { return companyFeignClient.getCompanyInfo("true", companyId); + } catch (RetryableException e) { + throw e; } catch (feign.FeignException.NotFound e) { throw new CompanyNotFoundException(); } catch (feign.FeignException e) { diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java index 26edecb..b5e4a0f 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/ProductClientAdapter.java @@ -32,7 +32,10 @@ public ProductInfo fetch(String ordererId, UUID productId, int quantity) { throw new InsufficientStockException(); } return info; - } catch (feign.FeignException.NotFound e) { + } catch (RetryableException e) { + throw e; + } + catch (feign.FeignException.NotFound e) { throw new ProductNotFoundException(); } catch (feign.FeignException e) { throw new ExternalServiceException(e); diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java index ee6895a..04d85bb 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/client/adapter/UserClientAdapter.java @@ -27,7 +27,10 @@ public class UserClientAdapter { public UserInfo fetch(UUID userId) { try { return userFeignClient.getUserInfo("true", userId); - } catch (feign.FeignException.NotFound e) { + }catch (RetryableException e) { + throw e; + } + catch (feign.FeignException.NotFound e) { throw new UserNotFoundException(); } catch (feign.FeignException e) { throw new ExternalServiceException(e);