├── order-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── data.sql │ │ │ ├── banner.txt │ │ │ ├── application-docker.properties │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── programmingtechie │ │ │ └── orderservice │ │ │ ├── repository │ │ │ └── OrderRepository.java │ │ │ ├── dto │ │ │ ├── OrderRequest.java │ │ │ ├── InventoryResponse.java │ │ │ └── OrderLineItemsDto.java │ │ │ ├── OrderServiceApplication.java │ │ │ ├── config │ │ │ ├── WebClientConfig.java │ │ │ └── ManualConfiguration.java │ │ │ ├── event │ │ │ └── OrderPlacedEvent.java │ │ │ ├── model │ │ │ ├── OrderLineItems.java │ │ │ └── Order.java │ │ │ ├── controller │ │ │ └── OrderController.java │ │ │ ├── listener │ │ │ └── OrderPlacedEventListener.java │ │ │ └── service │ │ │ └── OrderService.java │ └── test │ │ └── java │ │ └── com │ │ └── programmingtechie │ │ └── orderservice │ │ └── OrderServiceApplicationTests.java └── pom.xml ├── inventory-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── data.sql │ │ │ ├── application-docker.properties │ │ │ ├── banner.txt │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── programmingtechie │ │ │ └── inventoryservice │ │ │ ├── dto │ │ │ └── InventoryResponse.java │ │ │ ├── repository │ │ │ └── InventoryRepository.java │ │ │ ├── InventoryServiceApplication.java │ │ │ ├── model │ │ │ └── Inventory.java │ │ │ ├── util │ │ │ └── DataLoader.java │ │ │ ├── controller │ │ │ └── InventoryController.java │ │ │ └── service │ │ │ └── InventoryService.java │ └── test │ │ └── java │ │ └── com │ │ └── programmingtechie │ │ └── inventoryservice │ │ └── InventoryServiceApplicationTests.java └── pom.xml ├── api-gateway ├── Dockerfile ├── src │ └── main │ │ ├── resources │ │ ├── application-docker.properties │ │ └── application.properties │ │ └── java │ │ └── com │ │ └── programming │ │ └── techie │ │ └── apigateway │ │ ├── ApiGatewayApplication.java │ │ └── config │ │ └── SecurityConfig.java ├── Dockerfile.layered └── pom.xml ├── discovery-server ├── src │ └── main │ │ ├── resources │ │ ├── application-docker.properties │ │ └── application.properties │ │ └── java │ │ └── com │ │ └── programming │ │ └── techie │ │ └── discoveryserver │ │ ├── DiscoveryServerApplication.java │ │ └── SecurityConfig.java └── pom.xml ├── notification-service ├── src │ └── main │ │ ├── resources │ │ ├── application-docker.properties │ │ └── application.properties │ │ └── java │ │ └── com │ │ └── programming │ │ └── techie │ │ ├── OrderPlacedEvent.java │ │ ├── ManualConfiguration.java │ │ └── NotificationServiceApplication.java └── pom.xml ├── product-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── application-docker.properties │ │ │ ├── banner.txt │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── programmingtechie │ │ │ └── productservice │ │ │ ├── repository │ │ │ └── ProductRepository.java │ │ │ ├── ProductServiceApplication.java │ │ │ ├── dto │ │ │ ├── ProductRequest.java │ │ │ └── ProductResponse.java │ │ │ ├── model │ │ │ └── Product.java │ │ │ ├── util │ │ │ └── DataLoader.java │ │ │ ├── controller │ │ │ └── ProductController.java │ │ │ └── service │ │ │ └── ProductService.java │ └── test │ │ └── java │ │ └── com │ │ └── programmingtechie │ │ └── productservice │ │ └── ProductServiceApplicationTests.java └── pom.xml ├── .gitignore ├── k8s ├── infrastructure │ ├── zipkin │ │ └── zipkin-deployment.yaml │ ├── zookeeper │ │ └── zookeeper-deployment.yaml │ ├── discovery-server │ │ └── discovery-server-deployment.yaml │ ├── api-gateway │ │ └── api-gateway-deployment.yaml │ ├── mongodb │ │ └── mongo-deployment.yaml │ ├── keycloak-mysql │ │ └── keycloak-mysql-deployment.yaml │ ├── prometheus │ │ └── prometheus-deployment.yaml │ ├── kafka │ │ └── broker-deployment.yaml │ ├── postgres-inventory │ │ └── postgres-inventory-deployment.yaml │ ├── postgres-order │ │ └── postgres-order-deployment.yaml │ ├── grafana │ │ └── grafana-deployment.yaml │ └── keycloak │ │ └── keycloak-deployment.yaml └── services │ ├── product-service │ └── product-service-deployment.yaml │ ├── notification-service │ └── notification-service-deployment.yaml │ ├── order-service │ └── order-service-deployment.yaml │ └── inventory-service │ └── inventory-service-deployment.yaml ├── prometheus └── prometheus.yml ├── README.md ├── pom.xml ├── docker-compose.yml └── realms └── realm-export.json /order-service/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS 'order-service'; 2 | -------------------------------------------------------------------------------- /inventory-service/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS 'inventory-service'; 2 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17 2 | 3 | COPY target/*.jar app.jar 4 | 5 | ENTRYPOINT ["java","-jar","/app.jar"] 6 | -------------------------------------------------------------------------------- /discovery-server/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | server.port=8761 2 | management.zipkin.tracing.endpoint=http://zipkin:9411 3 | -------------------------------------------------------------------------------- /notification-service/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | management.zipkin.tracing.endpoint=http://zipkin:9411 2 | eureka.client.serviceUrl.defaultZone=http://discovery-server:8761/eureka 3 | spring.kafka.bootstrap-servers=broker:29092 4 | -------------------------------------------------------------------------------- /product-service/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | spring.data.mongodb.host=mongo 2 | spring.data.mongodb.port=27017 3 | spring.data.mongodb.database=product-service 4 | server.port=8080 5 | management.zipkin.tracing.endpoint=http://zipkin:9411 6 | eureka.client.serviceUrl.defaultZone=http://discovery-server:8761/eureka 7 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.repository; 2 | 3 | import com.programmingtechie.orderservice.model.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface OrderRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/programming/techie/OrderPlacedEvent.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class OrderPlacedEvent { 11 | private String orderNumber; 12 | } 13 | -------------------------------------------------------------------------------- /api-gateway/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | eureka.client.serviceUrl.defaultZone=http://eureka:password@discovery-server:8761/eureka 3 | spring.security.oauth2.resourceserver.jwt.issuer-uri= http://keycloak:8080/realms/spring-boot-microservices-realm 4 | management.zipkin.tracing.endpoint=http://zipkin:9411 5 | app.eureka-server=discovery-server 6 | -------------------------------------------------------------------------------- /order-service/src/test/java/com/programmingtechie/orderservice/OrderServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class OrderServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.repository; 2 | 3 | import com.programmingtechie.productservice.model.Product; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface ProductRepository extends MongoRepository { 7 | } 8 | -------------------------------------------------------------------------------- /inventory-service/src/test/java/com/programmingtechie/inventoryservice/InventoryServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class InventoryServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /order-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ _ ___ _ 2 | / _ \ _ _ __| | ___ _ _ / __| ___ _ _ __ __ (_) __ ___ 3 | | (_) | | '_| / _` | / -_) | '_| \__ \ / -_) | '_| \ V / | | / _| / -_) 4 | \___/ |_| \__,_| \___| |_| |___/ \___| |_| \_/ |_| \__| \___| 5 | 6 | ${application.title} ${application.version} 7 | Powered by Spring Boot ${spring-boot.version 8 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/dto/OrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class OrderRequest { 13 | private List orderLineItemsDtoList; 14 | } 15 | -------------------------------------------------------------------------------- /inventory-service/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | spring.datasource.url=jdbc:postgresql://postgres:5432/inventory-service 3 | spring.datasource.driver-class-name=org.postgresql.Driver 4 | spring.datasource.username=ptechie 5 | spring.datasource.password=password 6 | management.zipkin.tracing.endpoint=http://zipkin:9411 7 | eureka.client.serviceUrl.defaultZone=http://eureka:password@discovery-server:8761/eureka 8 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/dto/InventoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @Builder 12 | public class InventoryResponse { 13 | private String skuCode; 14 | private boolean isInStock; 15 | } 16 | -------------------------------------------------------------------------------- /order-service/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | spring.datasource.url=jdbc:postgresql://postgres:5431/order_service 3 | spring.datasource.driver-class-name=org.postgresql.Driver 4 | spring.datasource.username=ptechie 5 | spring.datasource.password=password 6 | management.zipkin.tracing.endpoint=http://zipkin:9411 7 | eureka.client.serviceUrl.defaultZone=http://discovery-server:8761/eureka 8 | spring.kafka.bootstrap-servers=broker:29092 9 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/dto/InventoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @Builder 12 | public class InventoryResponse { 13 | private String skuCode; 14 | private boolean isInStock; 15 | } 16 | -------------------------------------------------------------------------------- /product-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ _ _ ___ _ 2 | | _ \ _ _ ___ __| | _ _ __ | |_ / __| ___ _ _ __ __ (_) __ ___ 3 | | _/ | '_| / _ \ / _` | | || | / _| | _| \__ \ / -_) | '_| \ V / | | / _| / -_) 4 | |_| |_| \___/ \__,_| \_,_| \__| \__| |___/ \___| |_| \_/ |_| \__| \___| 5 | 6 | ${application.title} ${application.version} 7 | Powered by Spring Boot ${spring-boot.version} 8 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/repository/InventoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.repository; 2 | 3 | import com.programmingtechie.inventoryservice.model.Inventory; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface InventoryRepository extends JpaRepository { 9 | List findBySkuCodeIn(List skuCode); 10 | } 11 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/OrderServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class OrderServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(OrderServiceApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/ProductServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ProductServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ProductServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/InventoryServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class InventoryServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(InventoryServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/dto/OrderLineItemsDto.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class OrderLineItemsDto { 13 | private Long id; 14 | private String skuCode; 15 | private BigDecimal price; 16 | private Integer quantity; 17 | } 18 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/dto/ProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class ProductRequest { 15 | private String name; 16 | private String description; 17 | private BigDecimal price; 18 | } 19 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile.layered: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.4.1_1-jre as builder 2 | WORKDIR extracted 3 | ADD target/*.jar app.jar 4 | RUN java -Djarmode=layertools -jar app.jar extract 5 | 6 | FROM eclipse-temurin:17.0.4.1_1-jre 7 | WORKDIR application 8 | COPY --from=builder extracted/dependencies/ ./ 9 | COPY --from=builder extracted/spring-boot-loader/ ./ 10 | COPY --from=builder extracted/snapshot-dependencies/ ./ 11 | COPY --from=builder extracted/application/ ./ 12 | EXPOSE 8080 13 | ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] 14 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/dto/ProductResponse.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class ProductResponse { 15 | private String id; 16 | private String name; 17 | private String description; 18 | private BigDecimal price; 19 | } 20 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/programming/techie/apigateway/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie.apigateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class ApiGatewayApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(ApiGatewayApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /inventory-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ _ ___ _ 2 | |_ _| _ _ __ __ ___ _ _ | |_ ___ _ _ _ _ / __| ___ _ _ __ __ (_) __ ___ 3 | | | | ' \ \ V / / -_) | ' \ | _| / _ \ | '_| | || | \__ \ / -_) | '_| \ V / | | / _| / -_) 4 | |___| |_||_| \_/ \___| |_||_| \__| \___/ |_| \_, | |___/ \___| |_| \_/ |_| \__| \___| 5 | |__/ 6 | ${application.title} ${application.version} 7 | Powered by Spring Boot ${spring-boot.version} 8 | -------------------------------------------------------------------------------- /discovery-server/src/main/java/com/programming/techie/discoveryserver/DiscoveryServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie.discoveryserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @SpringBootApplication 8 | @EnableEurekaServer 9 | public class DiscoveryServerApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(DiscoveryServerApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/config/WebClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.config; 2 | 3 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.reactive.function.client.WebClient; 7 | 8 | @Configuration 9 | public class WebClientConfig { 10 | 11 | @Bean 12 | @LoadBalanced 13 | public WebClient.Builder webClientBuilder() { 14 | return WebClient.builder(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /product-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.data.mongodb.host=localhost 2 | spring.data.mongodb.port=27017 3 | spring.data.mongodb.database=product-service 4 | eureka.client.serviceUrl.defaultZone=http://eureka:password@localhost:8761/eureka 5 | spring.application.name=product-service 6 | server.port=0 7 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans 8 | management.tracing.sampling.probability=1.0 9 | # Actuator Prometheus Endpoint 10 | management.endpoints.web.exposure.include= prometheus 11 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 12 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/model/Inventory.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import jakarta.persistence.*; 9 | 10 | @Entity 11 | @Table 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class Inventory { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | private String skuCode; 22 | private Integer quantity; 23 | } 24 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/event/OrderPlacedEvent.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.event; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | 8 | @Getter 9 | @Setter 10 | public class OrderPlacedEvent extends ApplicationEvent { 11 | private String orderNumber; 12 | 13 | public OrderPlacedEvent(Object source, String orderNumber) { 14 | super(source); 15 | this.orderNumber = orderNumber; 16 | } 17 | 18 | public OrderPlacedEvent(String orderNumber) { 19 | super(orderNumber); 20 | this.orderNumber = orderNumber; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /discovery-server/src/main/java/com/programming/techie/discoveryserver/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie.discoveryserver; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.web.SecurityFilterChain; 7 | 8 | @Configuration 9 | public class SecurityConfig { 10 | @Bean 11 | public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { 12 | httpSecurity.csrf().ignoringRequestMatchers("/eureka/**"); 13 | return httpSecurity.build(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/model/OrderLineItems.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import jakarta.persistence.*; 9 | import java.math.BigDecimal; 10 | 11 | @Entity 12 | @Table(name = "t_order_line_items") 13 | @Getter 14 | @Setter 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class OrderLineItems { 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | private String skuCode; 22 | private BigDecimal price; 23 | private Integer quantity; 24 | } 25 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | import java.math.BigDecimal; 11 | 12 | @Document(value = "product") 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Builder 16 | @Data 17 | public class Product { 18 | 19 | @Id 20 | private String id; 21 | private String name; 22 | private String description; 23 | private BigDecimal price; 24 | } 25 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import jakarta.persistence.*; 9 | import java.util.List; 10 | 11 | @Entity 12 | @Table(name = "t_orders") 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class Order { 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | private String orderNumber; 22 | @OneToMany(cascade = CascadeType.ALL) 23 | private List orderLineItemsList; 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### My SQL Host Data ### 36 | my-sql-data 37 | mongo-data 38 | /mysql_keycloak_data/ 39 | /postgres/ 40 | /postgres-inventory/ 41 | /postgres-order/ 42 | /grafana/ 43 | -------------------------------------------------------------------------------- /discovery-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.instance.hostname=localhost 2 | eureka.client.register-with-eureka=false 3 | eureka.client.fetch-registry=false 4 | eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/ 5 | server.port=8761 6 | spring.security.user.name=eureka 7 | spring.security.user.password=password 8 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans 9 | management.tracing.sampling.probability=1.0 10 | spring.application.name=discovery-server 11 | # Actuator Prometheus Endpoint 12 | management.endpoints.web.exposure.include=prometheus 13 | logging.level.org.springframework.security=DEBUG 14 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 15 | -------------------------------------------------------------------------------- /inventory-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=0 2 | spring.datasource.url=jdbc:mysql://localhost:3306/inventory_service 3 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 4 | spring.datasource.username=root 5 | spring.datasource.password=mysql 6 | spring.jpa.hibernate.ddl-auto=update 7 | eureka.client.serviceUrl.defaultZone=http://eureka:password@localhost:8761/eureka 8 | spring.application.name=inventory-service 9 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans 10 | management.tracing.sampling.probability=1.0 11 | # Actuator Prometheus Endpoint 12 | management.endpoints.web.exposure.include= prometheus 13 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 14 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/config/ManualConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.config; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.kafka.core.KafkaTemplate; 7 | 8 | /** 9 | * In this class we'll add all the manual configuration required for Observability to 10 | * work. 11 | */ 12 | @Configuration(proxyBeanMethods = false) 13 | @RequiredArgsConstructor 14 | public class ManualConfiguration { 15 | 16 | private final KafkaTemplate kafkaTemplate; 17 | 18 | @PostConstruct 19 | void setup() { 20 | this.kafkaTemplate.setObservationEnabled(true); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /k8s/infrastructure/zipkin/zipkin-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: zipkin 6 | name: zipkin 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: zipkin 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: zipkin 18 | spec: 19 | containers: 20 | - image: openzipkin/zipkin 21 | name: zipkin 22 | ports: 23 | - containerPort: 9411 24 | resources: {} 25 | restartPolicy: Always 26 | status: {} 27 | --- 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | labels: 32 | app: zipkin 33 | name: zipkin 34 | spec: 35 | ports: 36 | - name: "9411" 37 | port: 9411 38 | targetPort: 9411 39 | selector: 40 | app: zipkin 41 | status: 42 | loadBalancer: {} 43 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/programming/techie/ManualConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; 7 | 8 | /** 9 | * In this class we'll add all the manual configuration required for Observability to 10 | * work. 11 | */ 12 | @Configuration(proxyBeanMethods = false) 13 | @RequiredArgsConstructor 14 | public class ManualConfiguration { 15 | 16 | private final ConcurrentKafkaListenerContainerFactory concurrentKafkaListenerContainerFactory; 17 | 18 | @PostConstruct 19 | void setup() { 20 | this.concurrentKafkaListenerContainerFactory.getContainerProperties().setObservationEnabled(true); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /k8s/infrastructure/zookeeper/zookeeper-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: zookeeper 5 | labels: 6 | app: zookeeper 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: zookeeper 12 | template: 13 | metadata: 14 | labels: 15 | app: zookeeper 16 | spec: 17 | containers: 18 | - env: 19 | - name: ZOOKEEPER_CLIENT_PORT 20 | value: "2181" 21 | - name: ZOOKEEPER_TICK_TIME 22 | value: "2000" 23 | image: confluentinc/cp-zookeeper:7.0.1 24 | name: zookeeper 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: zookeeper 30 | labels: 31 | app: zookeeper 32 | spec: 33 | selector: 34 | app: zookeeper 35 | ports: 36 | - port: 2181 37 | targetPort: 2181 38 | status: 39 | loadBalancer: {} 40 | -------------------------------------------------------------------------------- /k8s/services/product-service/product-service-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: product-service 6 | name: product-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: product-service 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: product-service 18 | spec: 19 | containers: 20 | - env: 21 | - name: SPRING_PROFILES_ACTIVE 22 | value: docker 23 | image: saiupadhyayula007/product-service:latest 24 | name: product-service 25 | resources: {} 26 | restartPolicy: Always 27 | status: {} 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | labels: 33 | app: product-service 34 | name: product-service 35 | spec: 36 | ports: 37 | - name: "80" 38 | port: 80 39 | targetPort: 9194 40 | selector: 41 | app: product-service 42 | status: 43 | loadBalancer: {} 44 | -------------------------------------------------------------------------------- /notification-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.serviceUrl.defaultZone=http://eureka:password@localhost:8761/eureka 2 | spring.application.name=notification-service 3 | server.port=0 4 | 5 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans/ 6 | management.tracing.sampling.probability=1 7 | 8 | # Kafka Properties 9 | spring.kafka.bootstrap-servers=localhost:9092 10 | spring.kafka.template.default-topic=notificationTopic 11 | spring.kafka.consumer.group-id= notificationId 12 | spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer 13 | spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer 14 | spring.kafka.consumer.properties.spring.json.type.mapping=event:com.programming.techie.OrderPlacedEvent 15 | 16 | # Actuator Prometheus Endpoint 17 | management.endpoints.web.exposure.include= prometheus 18 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 19 | -------------------------------------------------------------------------------- /prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'product_service' 7 | metrics_path: '/actuator/prometheus' 8 | static_configs: 9 | - targets: ['product-service:8080'] 10 | labels: 11 | application: 'Product Service Application' 12 | - job_name: 'order_service' 13 | metrics_path: '/actuator/prometheus' 14 | static_configs: 15 | - targets: ['order-service:8080'] 16 | labels: 17 | application: 'Order Service Application' 18 | - job_name: 'inventory_service' 19 | metrics_path: '/actuator/prometheus' 20 | static_configs: 21 | - targets: ['inventory-service:8080'] 22 | labels: 23 | application: 'Inventory Service Application' 24 | - job_name: 'notification_service' 25 | metrics_path: '/actuator/prometheus' 26 | static_configs: 27 | - targets: ['notification-service:8080'] 28 | labels: 29 | application: 'Notification Service Application' 30 | -------------------------------------------------------------------------------- /k8s/services/notification-service/notification-service-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: notification-service 6 | name: notification-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: notification-service 12 | strategy: {} 13 | template: 14 | metadata: 15 | labels: 16 | app: notification-service 17 | spec: 18 | containers: 19 | - env: 20 | - name: SPRING_PROFILES_ACTIVE 21 | value: docker 22 | image: saiupadhyayula007/notification-service:latest 23 | name: notification-service 24 | resources: {} 25 | restartPolicy: Always 26 | status: {} 27 | --- 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | labels: 32 | app: notification-service 33 | name: notification-service 34 | spec: 35 | ports: 36 | - name: "80" 37 | port: 80 38 | targetPort: 9192 39 | selector: 40 | app: notification-service 41 | status: 42 | loadBalancer: {} 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Microservices 2 | 3 | # ATTENTION: This repository is archived, you can find the source code in the new repository that includes much more concepts and upto date - https://github.com/SaiUpadhyayula/spring-boot-3-microservices-course 4 | 5 | The link to the new tutorial can be found here - https://www.youtube.com/playlist?list=PLSVW22jAG8pDeU80nDzbUgr8qqzEMppi8 6 | 7 | This repository contains the latest source code of the spring-boot-microservices tutorial 8 | 9 | You can watch the tutorial on Youtube here - https://www.youtube.com/watch?v=mPPhcU7oWDU&t=20634s 10 | 11 | ## How to run the application using Docker 12 | 13 | 1. Run `mvn clean package -DskipTests` to build the applications and create the docker image locally. 14 | 2. Run `docker-compose up -d` to start the applications. 15 | 16 | ## How to run the application without Docker 17 | 18 | 1. Run `mvn clean verify -DskipTests` by going inside each folder to build the applications. 19 | 2. After that run `mvn spring-boot:run` by going inside each folder to start the applications. 20 | 21 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/util/DataLoader.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.util; 2 | 3 | import com.programmingtechie.productservice.model.Product; 4 | import com.programmingtechie.productservice.repository.ProductRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.math.BigDecimal; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | public class DataLoader implements CommandLineRunner { 14 | 15 | private final ProductRepository productRepository; 16 | 17 | @Override 18 | public void run(String... args) throws Exception { 19 | if (productRepository.count() < 1) { 20 | Product product = new Product(); 21 | product.setName("iPhone 13"); 22 | product.setDescription("iPhone 13"); 23 | product.setPrice(BigDecimal.valueOf(1000)); 24 | 25 | productRepository.save(product); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/util/DataLoader.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.util; 2 | 3 | import com.programmingtechie.inventoryservice.model.Inventory; 4 | import com.programmingtechie.inventoryservice.repository.InventoryRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @RequiredArgsConstructor 11 | public class DataLoader implements CommandLineRunner { 12 | private final InventoryRepository inventoryRepository; 13 | @Override 14 | public void run(String... args) throws Exception { 15 | Inventory inventory = new Inventory(); 16 | inventory.setSkuCode("iphone_13"); 17 | inventory.setQuantity(100); 18 | 19 | Inventory inventory1 = new Inventory(); 20 | inventory1.setSkuCode("iphone_13_red"); 21 | inventory1.setQuantity(0); 22 | 23 | inventoryRepository.save(inventory); 24 | inventoryRepository.save(inventory1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /k8s/infrastructure/discovery-server/discovery-server-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: discovery-server 6 | name: discovery-server 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: discovery-server 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: discovery-server 18 | spec: 19 | containers: 20 | - env: 21 | - name: SPRING_PROFILES_ACTIVE 22 | value: docker 23 | image: saiupadhyayula007/discovery-server:latest 24 | name: discovery-server 25 | ports: 26 | - containerPort: 8761 27 | resources: {} 28 | restartPolicy: Always 29 | status: {} 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | labels: 35 | app: discovery-server 36 | name: discovery-server 37 | spec: 38 | ports: 39 | - name: "8761" 40 | port: 8761 41 | targetPort: 8761 42 | selector: 43 | app: discovery-server 44 | status: 45 | loadBalancer: {} 46 | 47 | -------------------------------------------------------------------------------- /k8s/services/order-service/order-service-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: order-service 6 | name: order-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: order-service 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: order-service 18 | spec: 19 | containers: 20 | - env: 21 | - name: SPRING_DATASOURCE_URL 22 | value: jdbc:postgresql://postgres-order:5431/order-service 23 | - name: SPRING_PROFILES_ACTIVE 24 | value: docker 25 | image: saiupadhyayula007/order-service:latest 26 | name: order-service 27 | resources: {} 28 | restartPolicy: Always 29 | status: {} 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | labels: 35 | app: order-service 36 | name: order-service 37 | spec: 38 | ports: 39 | - name: "80" 40 | port: 80 41 | targetPort: 9193 42 | selector: 43 | app: order-service 44 | status: 45 | loadBalancer: {} 46 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.controller; 2 | 3 | import com.programmingtechie.productservice.dto.ProductRequest; 4 | import com.programmingtechie.productservice.dto.ProductResponse; 5 | import com.programmingtechie.productservice.service.ProductService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/api/product") 14 | @RequiredArgsConstructor 15 | public class ProductController { 16 | 17 | private final ProductService productService; 18 | 19 | @PostMapping 20 | @ResponseStatus(HttpStatus.CREATED) 21 | public void createProduct(@RequestBody ProductRequest productRequest) { 22 | productService.createProduct(productRequest); 23 | } 24 | 25 | @GetMapping 26 | @ResponseStatus(HttpStatus.OK) 27 | public List getAllProducts() { 28 | return productService.getAllProducts(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /k8s/services/inventory-service/inventory-service-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: inventory-service 6 | name: inventory-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: inventory-service 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: inventory-service 18 | spec: 19 | containers: 20 | - env: 21 | - name: SPRING_DATASOURCE_URL 22 | value: jdbc:postgresql://postgres-inventory:5432/inventory-service 23 | - name: SPRING_PROFILES_ACTIVE 24 | value: docker 25 | image: saiupadhyayula007/inventory-service:latest 26 | name: inventory-service 27 | resources: {} 28 | restartPolicy: Always 29 | status: {} 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | labels: 35 | app: inventory-service 36 | name: inventory-service 37 | spec: 38 | ports: 39 | - name: "80" 40 | port: 80 41 | targetPort: 9191 42 | selector: 43 | app: inventory-service 44 | status: 45 | loadBalancer: {} 46 | -------------------------------------------------------------------------------- /k8s/infrastructure/api-gateway/api-gateway-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: api-gateway 6 | name: api-gateway 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: api-gateway 12 | strategy: {} 13 | template: 14 | metadata: 15 | labels: 16 | app: api-gateway 17 | spec: 18 | containers: 19 | - env: 20 | - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_SECURITY 21 | value: ' TRACE' 22 | - name: SPRING_PROFILES_ACTIVE 23 | value: docker 24 | image: saiupadhyayula007/api-gateway:latest 25 | name: api-gateway 26 | ports: 27 | - containerPort: 8080 28 | - containerPort: 8181 29 | resources: {} 30 | restartPolicy: Always 31 | status: {} 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | labels: 37 | app: api-gateway 38 | name: api-gateway 39 | spec: 40 | type: LoadBalancer 41 | ports: 42 | - name: "8181" 43 | port: 8181 44 | targetPort: 8080 45 | selector: 46 | app: api-gateway 47 | status: 48 | loadBalancer: {} 49 | 50 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/controller/InventoryController.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.controller; 2 | 3 | import com.programmingtechie.inventoryservice.dto.InventoryResponse; 4 | import com.programmingtechie.inventoryservice.service.InventoryService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/api/inventory") 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class InventoryController { 17 | 18 | private final InventoryService inventoryService; 19 | 20 | // http://localhost:8082/api/inventory/iphone-13,iphone13-red 21 | 22 | // http://localhost:8082/api/inventory?skuCode=iphone-13&skuCode=iphone13-red 23 | @GetMapping 24 | @ResponseStatus(HttpStatus.OK) 25 | public List isInStock(@RequestParam List skuCode) { 26 | log.info("Received inventory check request for skuCode: {}", skuCode); 27 | return inventoryService.isInStock(skuCode); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/programming/techie/apigateway/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie.apigateway.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.Customizer; 6 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 7 | import org.springframework.security.config.web.server.ServerHttpSecurity; 8 | import org.springframework.security.web.server.SecurityWebFilterChain; 9 | 10 | @Configuration 11 | @EnableWebFluxSecurity 12 | public class SecurityConfig { 13 | 14 | @Bean 15 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity serverHttpSecurity) { 16 | serverHttpSecurity 17 | .csrf(ServerHttpSecurity.CsrfSpec::disable) 18 | .authorizeExchange(exchange -> 19 | exchange.pathMatchers("/eureka/**") 20 | .permitAll() 21 | .anyExchange() 22 | .authenticated()) 23 | .oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults())); 24 | return serverHttpSecurity.build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /inventory-service/src/main/java/com/programmingtechie/inventoryservice/service/InventoryService.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.inventoryservice.service; 2 | 3 | import com.programmingtechie.inventoryservice.dto.InventoryResponse; 4 | import com.programmingtechie.inventoryservice.repository.InventoryRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class InventoryService { 17 | 18 | private final InventoryRepository inventoryRepository; 19 | 20 | @Transactional(readOnly = true) 21 | @SneakyThrows 22 | public List isInStock(List skuCode) { 23 | log.info("Checking Inventory"); 24 | return inventoryRepository.findBySkuCodeIn(skuCode).stream() 25 | .map(inventory -> 26 | InventoryResponse.builder() 27 | .skuCode(inventory.getSkuCode()) 28 | .isInStock(inventory.getQuantity() > 0) 29 | .build() 30 | ).toList(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /k8s/infrastructure/mongodb/mongo-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: mongo 6 | name: mongo 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: mongo 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: mongo 18 | spec: 19 | containers: 20 | - image: mongo:4.4.14-rc0-focal 21 | name: mongo 22 | ports: 23 | - containerPort: 27017 24 | resources: {} 25 | volumeMounts: 26 | - mountPath: /data/db 27 | name: mongo-claim0 28 | restartPolicy: Always 29 | volumes: 30 | - name: mongo-claim0 31 | persistentVolumeClaim: 32 | claimName: mongo-claim0 33 | status: {} 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | labels: 39 | app: mongo 40 | name: mongo 41 | spec: 42 | ports: 43 | - name: "27017" 44 | port: 27017 45 | targetPort: 27017 46 | selector: 47 | app: mongo 48 | status: 49 | loadBalancer: {} 50 | --- 51 | apiVersion: v1 52 | kind: PersistentVolumeClaim 53 | metadata: 54 | labels: 55 | app: mongo-claim0 56 | name: mongo-claim0 57 | spec: 58 | accessModes: 59 | - ReadWriteOnce 60 | resources: 61 | requests: 62 | storage: 100Mi 63 | status: {} 64 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/programming/techie/NotificationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.programming.techie; 2 | 3 | import io.micrometer.observation.Observation; 4 | import io.micrometer.observation.ObservationRegistry; 5 | import io.micrometer.tracing.Tracer; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.kafka.annotation.KafkaListener; 11 | 12 | @SpringBootApplication 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | public class NotificationServiceApplication { 16 | 17 | private final ObservationRegistry observationRegistry; 18 | private final Tracer tracer; 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(NotificationServiceApplication.class, args); 22 | } 23 | 24 | @KafkaListener(topics = "notificationTopic") 25 | public void handleNotification(OrderPlacedEvent orderPlacedEvent) { 26 | Observation.createNotStarted("on-message", this.observationRegistry).observe(() -> { 27 | log.info("Got message <{}>", orderPlacedEvent); 28 | log.info("TraceId- {}, Received Notification for Order - {}", this.tracer.currentSpan().context().traceId(), 29 | orderPlacedEvent.getOrderNumber()); 30 | }); 31 | // send out an email notification 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /k8s/infrastructure/keycloak-mysql/keycloak-mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: keycloak-mysql 6 | name: keycloak-mysql 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: keycloak-mysql 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: keycloak-mysql 18 | spec: 19 | containers: 20 | - env: 21 | - name: MYSQL_DATABASE 22 | value: keycloak 23 | - name: MYSQL_PASSWORD 24 | value: password 25 | - name: MYSQL_ROOT_PASSWORD 26 | value: root 27 | - name: MYSQL_USER 28 | value: keycloak 29 | image: mysql:5.7 30 | name: keycloak-mysql 31 | resources: {} 32 | volumeMounts: 33 | - mountPath: /var/lib/mysql 34 | name: keycloak-mysql-claim0 35 | restartPolicy: Always 36 | volumes: 37 | - name: keycloak-mysql-claim0 38 | persistentVolumeClaim: 39 | claimName: keycloak-mysql-claim0 40 | status: {} 41 | --- 42 | apiVersion: v1 43 | kind: PersistentVolumeClaim 44 | metadata: 45 | creationTimestamp: null 46 | labels: 47 | app: keycloak-mysql-claim0 48 | name: keycloak-mysql-claim0 49 | spec: 50 | accessModes: 51 | - ReadWriteOnce 52 | resources: 53 | requests: 54 | storage: 100Mi 55 | status: {} 56 | -------------------------------------------------------------------------------- /api-gateway/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=api-gateway 2 | eureka.client.serviceUrl.defaultZone=http://eureka:password@localhost:8761/eureka 3 | app.eureka-server=localhost 4 | 5 | ## Product Service Route 6 | spring.cloud.gateway.routes[0].id=product-service 7 | spring.cloud.gateway.routes[0].uri=lb://product-service 8 | spring.cloud.gateway.routes[0].predicates[0]=Path=/api/product 9 | 10 | ## Order Service Route 11 | spring.cloud.gateway.routes[1].id=order-service 12 | spring.cloud.gateway.routes[1].uri=lb://order-service 13 | spring.cloud.gateway.routes[1].predicates[0]=Path=/api/order 14 | 15 | ## Discover Server Route 16 | spring.cloud.gateway.routes[2].id=discovery-server 17 | spring.cloud.gateway.routes[2].uri=http://eureka:password@${app.eureka-server}:8761 18 | spring.cloud.gateway.routes[2].predicates[0]=Path=/eureka/web 19 | spring.cloud.gateway.routes[2].filters[0]=SetPath=/ 20 | 21 | ## Discover Server Static Resources Route 22 | spring.cloud.gateway.routes[3].id=discovery-server-static 23 | spring.cloud.gateway.routes[3].uri=http://eureka:password@${app.eureka-server}:8761 24 | spring.cloud.gateway.routes[3].predicates[0]=Path=/eureka/** 25 | 26 | spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:8080/realms/spring-boot-microservices-realm 27 | 28 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans 29 | management.tracing.sampling.probability=1.0 30 | 31 | # Actuator Prometheus Endpoint 32 | management.endpoints.web.exposure.include= prometheus 33 | server.port=8181 34 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 35 | -------------------------------------------------------------------------------- /k8s/infrastructure/prometheus/prometheus-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: F:\tools\kompose.exe convert 6 | kompose.version: 1.26.1 (a9d05d509) 7 | creationTimestamp: null 8 | labels: 9 | app: prometheus 10 | name: prometheus 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: prometheus 16 | strategy: 17 | type: Recreate 18 | template: 19 | metadata: 20 | 21 | labels: 22 | app: prometheus 23 | spec: 24 | containers: 25 | - image: prom/prometheus:v2.37.1 26 | name: prometheus 27 | ports: 28 | - containerPort: 9090 29 | resources: {} 30 | volumeMounts: 31 | - mountPath: /etc/prometheus/prometheus.yml 32 | name: prometheus-claim0 33 | restartPolicy: Always 34 | volumes: 35 | - name: prometheus-claim0 36 | persistentVolumeClaim: 37 | claimName: prometheus-claim0 38 | status: {} 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | labels: 44 | app: prometheus 45 | name: prometheus 46 | spec: 47 | ports: 48 | - name: "9090" 49 | port: 9090 50 | targetPort: 9090 51 | selector: 52 | app: prometheus 53 | status: 54 | loadBalancer: {} 55 | --- 56 | apiVersion: v1 57 | kind: PersistentVolumeClaim 58 | metadata: 59 | creationTimestamp: null 60 | labels: 61 | app: prometheus-claim0 62 | name: prometheus-claim0 63 | spec: 64 | accessModes: 65 | - ReadWriteOnce 66 | resources: 67 | requests: 68 | storage: 100Mi 69 | status: {} 70 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.controller; 2 | 3 | import com.programmingtechie.orderservice.dto.OrderRequest; 4 | import com.programmingtechie.orderservice.service.OrderService; 5 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 6 | import io.github.resilience4j.retry.annotation.Retry; 7 | import io.github.resilience4j.timelimiter.annotation.TimeLimiter; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | @RestController 16 | @RequestMapping("/api/order") 17 | @RequiredArgsConstructor 18 | @Slf4j 19 | public class OrderController { 20 | 21 | private final OrderService orderService; 22 | 23 | @PostMapping 24 | @ResponseStatus(HttpStatus.CREATED) 25 | @CircuitBreaker(name = "inventory", fallbackMethod = "fallbackMethod") 26 | @TimeLimiter(name = "inventory") 27 | @Retry(name = "inventory") 28 | public CompletableFuture placeOrder(@RequestBody OrderRequest orderRequest) { 29 | log.info("Placing Order"); 30 | return CompletableFuture.supplyAsync(() -> orderService.placeOrder(orderRequest)); 31 | } 32 | 33 | public CompletableFuture fallbackMethod(OrderRequest orderRequest, RuntimeException runtimeException) { 34 | log.info("Cannot Place Order Executing Fallback logic"); 35 | return CompletableFuture.supplyAsync(() -> "Oops! Something went wrong, please order after some time!"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /k8s/infrastructure/kafka/broker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: broker 6 | name: broker 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: broker 12 | strategy: {} 13 | template: 14 | metadata: 15 | 16 | labels: 17 | app: broker 18 | spec: 19 | containers: 20 | - env: 21 | - name: KAFKA_ADVERTISED_LISTENERS 22 | value: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 23 | - name: KAFKA_BROKER_ID 24 | value: "1" 25 | - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP 26 | value: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT 27 | - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR 28 | value: "1" 29 | - name: KAFKA_TRANSACTION_STATE_LOG_MIN_ISR 30 | value: "1" 31 | - name: KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR 32 | value: "1" 33 | - name: KAFKA_ZOOKEEPER_CONNECT 34 | value: zookeeper:2181 35 | image: confluentinc/cp-kafka:7.0.1 36 | name: broker 37 | ports: 38 | - containerPort: 9092 39 | resources: {} 40 | restartPolicy: Always 41 | status: {} 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | annotations: 47 | kompose.cmd: F:\tools\kompose.exe convert 48 | kompose.version: 1.26.1 (a9d05d509) 49 | creationTimestamp: null 50 | labels: 51 | app: broker 52 | name: broker 53 | spec: 54 | ports: 55 | - name: "9092" 56 | port: 9092 57 | targetPort: 9092 58 | selector: 59 | app: broker 60 | status: 61 | loadBalancer: {} 62 | -------------------------------------------------------------------------------- /product-service/src/main/java/com/programmingtechie/productservice/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice.service; 2 | 3 | import com.programmingtechie.productservice.dto.ProductRequest; 4 | import com.programmingtechie.productservice.dto.ProductResponse; 5 | import com.programmingtechie.productservice.model.Product; 6 | import com.programmingtechie.productservice.repository.ProductRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class ProductService { 17 | 18 | private final ProductRepository productRepository; 19 | 20 | public void createProduct(ProductRequest productRequest) { 21 | Product product = Product.builder() 22 | .name(productRequest.getName()) 23 | .description(productRequest.getDescription()) 24 | .price(productRequest.getPrice()) 25 | .build(); 26 | 27 | productRepository.save(product); 28 | log.info("Product {} is saved", product.getId()); 29 | } 30 | 31 | public List getAllProducts() { 32 | List products = productRepository.findAll(); 33 | 34 | return products.stream().map(this::mapToProductResponse).toList(); 35 | } 36 | 37 | private ProductResponse mapToProductResponse(Product product) { 38 | return ProductResponse.builder() 39 | .id(product.getId()) 40 | .name(product.getName()) 41 | .description(product.getDescription()) 42 | .price(product.getPrice()) 43 | .build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /k8s/infrastructure/postgres-inventory/postgres-inventory-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: postgres-inventory 6 | name: postgres-inventory 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: postgres-inventory 12 | template: 13 | metadata: 14 | labels: 15 | app: postgres-inventory 16 | spec: 17 | containers: 18 | - env: 19 | - name: PGDATA 20 | value: /data/postgres 21 | - name: POSTGRES_DB 22 | value: inventory-service 23 | - name: POSTGRES_PASSWORD 24 | value: password 25 | - name: POSTGRES_USER 26 | value: ptechie 27 | image: postgres 28 | name: postgres-inventory 29 | ports: 30 | - containerPort: 5432 31 | resources: {} 32 | volumeMounts: 33 | - mountPath: /data/postgres 34 | name: postgres-inventory-claim0 35 | restartPolicy: Always 36 | volumes: 37 | - name: postgres-inventory-claim0 38 | persistentVolumeClaim: 39 | claimName: postgres-inventory-claim0 40 | 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | labels: 46 | app: postgres-inventory 47 | name: postgres-inventory 48 | spec: 49 | ports: 50 | - name: "5432" 51 | port: 5432 52 | targetPort: 5432 53 | selector: 54 | app: postgres-inventory 55 | status: 56 | loadBalancer: {} 57 | --- 58 | apiVersion: v1 59 | kind: PersistentVolumeClaim 60 | metadata: 61 | labels: 62 | app: postgres-inventory-claim0 63 | name: postgres-inventory-claim0 64 | spec: 65 | accessModes: 66 | - ReadWriteOnce 67 | resources: 68 | requests: 69 | storage: 100Mi 70 | status: {} 71 | -------------------------------------------------------------------------------- /k8s/infrastructure/postgres-order/postgres-order-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: postgres-order 6 | name: postgres-order 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: postgres-order 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: postgres-order 18 | spec: 19 | containers: 20 | - args: 21 | - -p 22 | - "5431" 23 | env: 24 | - name: PGDATA 25 | value: /data/postgres 26 | - name: POSTGRES_DB 27 | value: order-service 28 | - name: POSTGRES_PASSWORD 29 | value: password 30 | - name: POSTGRES_USER 31 | value: ptechie 32 | image: postgres 33 | name: postgres-order 34 | ports: 35 | - containerPort: 5431 36 | resources: {} 37 | volumeMounts: 38 | - mountPath: /data/postgres 39 | name: postgres-order-claim0 40 | restartPolicy: Always 41 | volumes: 42 | - name: postgres-order-claim0 43 | persistentVolumeClaim: 44 | claimName: postgres-order-claim0 45 | status: {} 46 | --- 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | labels: 51 | app: postgres-order 52 | name: postgres-order 53 | spec: 54 | ports: 55 | - name: "5431" 56 | port: 5431 57 | targetPort: 5431 58 | selector: 59 | app: postgres-order 60 | status: 61 | loadBalancer: {} 62 | --- 63 | apiVersion: v1 64 | kind: PersistentVolumeClaim 65 | metadata: 66 | labels: 67 | app: postgres-order-claim0 68 | name: postgres-order-claim0 69 | spec: 70 | accessModes: 71 | - ReadWriteOnce 72 | resources: 73 | requests: 74 | storage: 100Mi 75 | status: {} 76 | -------------------------------------------------------------------------------- /discovery-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | discovery-server 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework.cloud 22 | spring-cloud-starter-netflix-eureka-server 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-security 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | io.micrometer 34 | micrometer-tracing-bridge-brave 35 | 36 | 37 | io.zipkin.reporter2 38 | zipkin-reporter-brave 39 | 40 | 41 | io.micrometer 42 | micrometer-registry-prometheus 43 | runtime 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /k8s/infrastructure/grafana/grafana-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: F:\tools\kompose.exe convert 6 | kompose.version: 1.26.1 (a9d05d509) 7 | creationTimestamp: null 8 | labels: 9 | app: grafana 10 | name: grafana 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: grafana 16 | strategy: 17 | type: Recreate 18 | template: 19 | metadata: 20 | labels: 21 | app: grafana 22 | spec: 23 | containers: 24 | - env: 25 | - name: GF_SECURITY_ADMIN_PASSWORD 26 | value: password 27 | - name: GF_SECURITY_ADMIN_USER 28 | value: admin 29 | image: grafana/grafana-oss:8.5.2 30 | name: grafana 31 | ports: 32 | - containerPort: 3000 33 | resources: {} 34 | volumeMounts: 35 | - mountPath: /var/lib/grafana 36 | name: grafana-claim0 37 | restartPolicy: Always 38 | volumes: 39 | - name: grafana-claim0 40 | persistentVolumeClaim: 41 | claimName: grafana-claim0 42 | status: {} 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | annotations: 48 | kompose.cmd: F:\tools\kompose.exe convert 49 | kompose.version: 1.26.1 (a9d05d509) 50 | creationTimestamp: null 51 | labels: 52 | app: grafana 53 | name: grafana 54 | spec: 55 | ports: 56 | - name: "3000" 57 | port: 3000 58 | targetPort: 3000 59 | selector: 60 | app: grafana 61 | status: 62 | loadBalancer: {} 63 | --- 64 | apiVersion: v1 65 | kind: PersistentVolumeClaim 66 | metadata: 67 | creationTimestamp: null 68 | labels: 69 | app: grafana-claim0 70 | name: grafana-claim0 71 | spec: 72 | accessModes: 73 | - ReadWriteOnce 74 | resources: 75 | requests: 76 | storage: 100Mi 77 | status: {} 78 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/listener/OrderPlacedEventListener.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.listener; 2 | 3 | import com.programmingtechie.orderservice.event.OrderPlacedEvent; 4 | import io.micrometer.observation.Observation; 5 | import io.micrometer.observation.ObservationRegistry; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.event.EventListener; 9 | import org.springframework.kafka.core.KafkaTemplate; 10 | import org.springframework.kafka.support.SendResult; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.ExecutionException; 15 | 16 | @Component 17 | @RequiredArgsConstructor 18 | @Slf4j 19 | public class OrderPlacedEventListener { 20 | 21 | private final KafkaTemplate kafkaTemplate; 22 | private final ObservationRegistry observationRegistry; 23 | 24 | @EventListener 25 | public void handleOrderPlacedEvent(OrderPlacedEvent event) { 26 | log.info("Order Placed Event Received, Sending OrderPlacedEvent to notificationTopic: {}", event.getOrderNumber()); 27 | 28 | // Create Observation for Kafka Template 29 | try { 30 | Observation.createNotStarted("notification-topic", this.observationRegistry).observeChecked(() -> { 31 | CompletableFuture> future = kafkaTemplate.send("notificationTopic", 32 | new OrderPlacedEvent(event.getOrderNumber())); 33 | return future.handle((result, throwable) -> CompletableFuture.completedFuture(result)); 34 | }).get(); 35 | } catch (InterruptedException | ExecutionException e) { 36 | Thread.currentThread().interrupt(); 37 | throw new RuntimeException("Error while sending message to Kafka", e); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /order-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 2 | spring.datasource.url=jdbc:mysql://localhost:3306/order_service 3 | spring.datasource.username=root 4 | spring.datasource.password=mysql 5 | spring.jpa.hibernate.ddl-auto=update 6 | 7 | server.port=8081 8 | eureka.client.serviceUrl.defaultZone=http://eureka:password@localhost:8761/eureka 9 | spring.application.name=order-service 10 | 11 | management.health.circuitbreakers.enabled=true 12 | management.endpoints.web.exposure.include=* 13 | management.endpoint.health.show-details=always 14 | 15 | #Resilinece4j Properties 16 | resilience4j.circuitbreaker.instances.inventory.registerHealthIndicator=true 17 | resilience4j.circuitbreaker.instances.inventory.event-consumer-buffer-size=10 18 | resilience4j.circuitbreaker.instances.inventory.slidingWindowType=COUNT_BASED 19 | resilience4j.circuitbreaker.instances.inventory.slidingWindowSize=5 20 | resilience4j.circuitbreaker.instances.inventory.failureRateThreshold=50 21 | resilience4j.circuitbreaker.instances.inventory.waitDurationInOpenState=5s 22 | resilience4j.circuitbreaker.instances.inventory.permittedNumberOfCallsInHalfOpenState=3 23 | resilience4j.circuitbreaker.instances.inventory.automaticTransitionFromOpenToHalfOpenEnabled=true 24 | 25 | #Resilience4J Timeout Properties 26 | resilience4j.timelimiter.instances.inventory.timeout-duration=3s 27 | 28 | #Resilience4J Retry Properties 29 | resilience4j.retry.instances.inventory.max-attempts=3 30 | resilience4j.retry.instances.inventory.wait-duration=5s 31 | 32 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans 33 | management.tracing.sampling.probability= 1.0 34 | 35 | # Kafka Properties 36 | spring.kafka.bootstrap-servers=localhost:9092 37 | spring.kafka.template.default-topic=notificationTopic 38 | spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer 39 | spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer 40 | spring.kafka.producer.properties.spring.json.type.mapping=event:com.programmingtechie.orderservice.event.OrderPlacedEvent 41 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] 42 | -------------------------------------------------------------------------------- /api-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | api-gateway 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework.cloud 22 | spring-cloud-starter-gateway 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-eureka-client 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-oauth2-resource-server 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-security 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-actuator 39 | 40 | 41 | io.micrometer 42 | micrometer-tracing-bridge-brave 43 | 44 | 45 | io.zipkin.reporter2 46 | zipkin-reporter-brave 47 | 48 | 49 | 50 | io.micrometer 51 | micrometer-registry-prometheus 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /k8s/infrastructure/keycloak/keycloak-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: keycloak 6 | name: keycloak 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: keycloak 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | 17 | labels: 18 | app: keycloak 19 | spec: 20 | containers: 21 | - args: 22 | - start-dev 23 | - --import-realm 24 | env: 25 | - name: DB_ADDR 26 | value: mysql 27 | - name: DB_DATABASE 28 | value: keycloak 29 | - name: DB_PASSWORD 30 | value: password 31 | - name: DB_USER 32 | value: keycloak 33 | - name: DB_VENDOR 34 | value: MYSQL 35 | - name: KEYCLOAK_ADMIN 36 | value: admin 37 | - name: KEYCLOAK_ADMIN_PASSWORD 38 | value: admin 39 | image: quay.io/keycloak/keycloak:18.0.0 40 | name: keycloak 41 | ports: 42 | - containerPort: 8080 43 | resources: {} 44 | volumeMounts: 45 | - mountPath: /opt/keycloak/data/import/ 46 | name: keycloak-claim0 47 | restartPolicy: Always 48 | volumes: 49 | - name: keycloak-claim0 50 | persistentVolumeClaim: 51 | claimName: keycloak-claim0 52 | status: {} 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | labels: 58 | app: keycloak 59 | name: keycloak 60 | spec: 61 | ports: 62 | - name: "8080" 63 | port: 8080 64 | targetPort: 8080 65 | selector: 66 | app: keycloak 67 | status: 68 | loadBalancer: {} 69 | --- 70 | apiVersion: v1 71 | kind: PersistentVolumeClaim 72 | metadata: 73 | creationTimestamp: null 74 | labels: 75 | app: keycloak-claim0 76 | name: keycloak-claim0 77 | spec: 78 | accessModes: 79 | - ReadWriteOnce 80 | resources: 81 | requests: 82 | storage: 100Mi 83 | status: {} 84 | --- 85 | apiVersion: networking.k8s.io/v1 86 | kind: Ingress 87 | metadata: 88 | name: auth-ingress 89 | spec: 90 | rules: 91 | - host: "auth.localtest.me" 92 | http: 93 | paths: 94 | - pathType: Prefix 95 | path: "/" 96 | backend: 97 | service: 98 | name: keycloak 99 | port: 100 | number: 8080 101 | -------------------------------------------------------------------------------- /notification-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | notification-service 13 | 14 | 15 | 17 16 | 17 17 | 17 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.kafka 27 | spring-kafka 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-starter-netflix-eureka-client 32 | 33 | 34 | org.projectlombok 35 | lombok 36 | true 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | org.springframework.kafka 45 | spring-kafka-test 46 | test 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-actuator 51 | 52 | 53 | io.micrometer 54 | micrometer-tracing-bridge-brave 55 | 56 | 57 | io.zipkin.reporter2 58 | zipkin-reporter-brave 59 | 60 | 61 | io.micrometer 62 | micrometer-registry-prometheus 63 | runtime 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /product-service/src/test/java/com/programmingtechie/productservice/ProductServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.productservice; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.programmingtechie.productservice.dto.ProductRequest; 5 | import com.programmingtechie.productservice.repository.ProductRepository; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.DynamicPropertyRegistry; 13 | import org.springframework.test.context.DynamicPropertySource; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 16 | import org.testcontainers.containers.MongoDBContainer; 17 | import org.testcontainers.junit.jupiter.Container; 18 | 19 | import java.math.BigDecimal; 20 | 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 22 | 23 | @SpringBootTest 24 | @AutoConfigureMockMvc 25 | class ProductServiceApplicationTests { 26 | 27 | @Container 28 | static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2"); 29 | @Autowired 30 | private MockMvc mockMvc; 31 | @Autowired 32 | private ObjectMapper objectMapper; 33 | @Autowired 34 | private ProductRepository productRepository; 35 | 36 | static { 37 | mongoDBContainer.start(); 38 | } 39 | 40 | @DynamicPropertySource 41 | static void setProperties(DynamicPropertyRegistry dymDynamicPropertyRegistry) { 42 | dymDynamicPropertyRegistry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); 43 | } 44 | 45 | @Test 46 | void shouldCreateProduct() throws Exception { 47 | ProductRequest productRequest = getProductRequest(); 48 | String productRequestString = objectMapper.writeValueAsString(productRequest); 49 | mockMvc.perform(MockMvcRequestBuilders.post("/api/product") 50 | .contentType(MediaType.APPLICATION_JSON) 51 | .content(productRequestString)) 52 | .andExpect(status().isCreated()); 53 | Assertions.assertEquals(1, productRepository.findAll().size()); 54 | } 55 | 56 | private ProductRequest getProductRequest() { 57 | return ProductRequest.builder() 58 | .name("iPhone 13") 59 | .description("iPhone 13") 60 | .price(BigDecimal.valueOf(1200)) 61 | .build(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /inventory-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | inventory-service 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-starter-netflix-eureka-client 31 | 32 | 33 | com.mysql 34 | mysql-connector-j 35 | runtime 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | true 41 | 42 | 43 | org.postgresql 44 | postgresql 45 | runtime 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-actuator 56 | 57 | 58 | io.micrometer 59 | micrometer-tracing-bridge-brave 60 | 61 | 62 | io.zipkin.reporter2 63 | zipkin-reporter-brave 64 | 65 | 66 | io.micrometer 67 | micrometer-registry-prometheus 68 | runtime 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /product-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | product-service 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-mongodb 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-starter-netflix-eureka-client 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | true 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | org.testcontainers 44 | mongodb 45 | test 46 | 47 | 48 | org.testcontainers 49 | junit-jupiter 50 | test 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-testcontainers 55 | test 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-actuator 60 | 61 | 62 | io.micrometer 63 | micrometer-tracing-bridge-brave 64 | 65 | 66 | io.zipkin.reporter2 67 | zipkin-reporter-brave 68 | 69 | 70 | io.micrometer 71 | micrometer-registry-prometheus 72 | runtime 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.2.1 11 | 12 | 13 | 14 | com.programming.techie 15 | microservices-new 16 | pom 17 | 1.0-SNAPSHOT 18 | 19 | product-service 20 | order-service 21 | inventory-service 22 | discovery-server 23 | api-gateway 24 | notification-service 25 | 26 | 27 | 28 | 17 29 | 17 30 | 2023.0.0 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-dependencies 38 | ${spring-cloud.version} 39 | pom 40 | import 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | 53 | org.projectlombok 54 | lombok 55 | 56 | 57 | 58 | 59 | 60 | com.google.cloud.tools 61 | jib-maven-plugin 62 | 3.2.1 63 | 64 | 65 | eclipse-temurin:17.0.4.1_1-jre 66 | 67 | 68 | microservices-tutorial/${project.artifactId} 69 | 70 | 71 | 72 | 73 | package 74 | 75 | dockerBuild 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /order-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | microservices-new 7 | com.programming.techie 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | order-service 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-webflux 31 | 32 | 33 | org.springframework.cloud 34 | spring-cloud-starter-netflix-eureka-client 35 | 36 | 37 | org.springframework.cloud 38 | spring-cloud-starter-circuitbreaker-resilience4j 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-actuator 43 | 44 | 45 | com.mysql 46 | mysql-connector-j 47 | runtime 48 | 49 | 50 | org.postgresql 51 | postgresql 52 | runtime 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | true 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | test 63 | 64 | 65 | 66 | org.springframework.kafka 67 | spring-kafka 68 | 69 | 70 | org.springframework.kafka 71 | spring-kafka-test 72 | test 73 | 74 | 75 | io.micrometer 76 | micrometer-tracing-bridge-brave 77 | 78 | 79 | io.zipkin.reporter2 80 | zipkin-reporter-brave 81 | 82 | 83 | io.micrometer 84 | micrometer-registry-prometheus 85 | runtime 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /order-service/src/main/java/com/programmingtechie/orderservice/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package com.programmingtechie.orderservice.service; 2 | 3 | import com.programmingtechie.orderservice.dto.InventoryResponse; 4 | import com.programmingtechie.orderservice.dto.OrderLineItemsDto; 5 | import com.programmingtechie.orderservice.dto.OrderRequest; 6 | import com.programmingtechie.orderservice.event.OrderPlacedEvent; 7 | import com.programmingtechie.orderservice.model.Order; 8 | import com.programmingtechie.orderservice.model.OrderLineItems; 9 | import com.programmingtechie.orderservice.repository.OrderRepository; 10 | import io.micrometer.observation.Observation; 11 | import io.micrometer.observation.ObservationRegistry; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.context.ApplicationEventPublisher; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | import org.springframework.web.reactive.function.client.WebClient; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | import java.util.UUID; 22 | 23 | @Service 24 | @RequiredArgsConstructor 25 | @Transactional 26 | @Slf4j 27 | public class OrderService { 28 | 29 | private final OrderRepository orderRepository; 30 | private final WebClient.Builder webClientBuilder; 31 | private final ObservationRegistry observationRegistry; 32 | private final ApplicationEventPublisher applicationEventPublisher; 33 | 34 | public String placeOrder(OrderRequest orderRequest) { 35 | Order order = new Order(); 36 | order.setOrderNumber(UUID.randomUUID().toString()); 37 | 38 | List orderLineItems = orderRequest.getOrderLineItemsDtoList() 39 | .stream() 40 | .map(this::mapToDto) 41 | .toList(); 42 | 43 | order.setOrderLineItemsList(orderLineItems); 44 | 45 | List skuCodes = order.getOrderLineItemsList().stream() 46 | .map(OrderLineItems::getSkuCode) 47 | .toList(); 48 | 49 | // Call Inventory Service, and place order if product is in 50 | // stock 51 | Observation inventoryServiceObservation = Observation.createNotStarted("inventory-service-lookup", 52 | this.observationRegistry); 53 | inventoryServiceObservation.lowCardinalityKeyValue("call", "inventory-service"); 54 | return inventoryServiceObservation.observe(() -> { 55 | InventoryResponse[] inventoryResponseArray = webClientBuilder.build().get() 56 | .uri("http://inventory-service/api/inventory", 57 | uriBuilder -> uriBuilder.queryParam("skuCode", skuCodes).build()) 58 | .retrieve() 59 | .bodyToMono(InventoryResponse[].class) 60 | .block(); 61 | 62 | boolean allProductsInStock = Arrays.stream(inventoryResponseArray) 63 | .allMatch(InventoryResponse::isInStock); 64 | 65 | if (allProductsInStock) { 66 | orderRepository.save(order); 67 | // publish Order Placed Event 68 | applicationEventPublisher.publishEvent(new OrderPlacedEvent(this, order.getOrderNumber())); 69 | return "Order Placed"; 70 | } else { 71 | throw new IllegalArgumentException("Product is not in stock, please try again later"); 72 | } 73 | }); 74 | 75 | } 76 | 77 | private OrderLineItems mapToDto(OrderLineItemsDto orderLineItemsDto) { 78 | OrderLineItems orderLineItems = new OrderLineItems(); 79 | orderLineItems.setPrice(orderLineItemsDto.getPrice()); 80 | orderLineItems.setQuantity(orderLineItemsDto.getQuantity()); 81 | orderLineItems.setSkuCode(orderLineItemsDto.getSkuCode()); 82 | return orderLineItems; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.7' 3 | services: 4 | ## MySQL Docker Compose Config 5 | postgres-order: 6 | container_name: postgres-order 7 | image: postgres 8 | environment: 9 | POSTGRES_DB: order-service 10 | POSTGRES_USER: ptechie 11 | POSTGRES_PASSWORD: password 12 | PGDATA: /data/postgres 13 | volumes: 14 | - ./postgres-order:/data/postgres 15 | expose: 16 | - "5431" 17 | ports: 18 | - "5431:5431" 19 | command: -p 5431 20 | restart: always 21 | 22 | postgres-inventory: 23 | container_name: postgres-inventory 24 | image: postgres 25 | environment: 26 | POSTGRES_DB: inventory-service 27 | POSTGRES_USER: ptechie 28 | POSTGRES_PASSWORD: password 29 | PGDATA: /data/postgres 30 | volumes: 31 | - ./postgres-inventory:/data/postgres 32 | ports: 33 | - "5432:5432" 34 | restart: always 35 | 36 | ## Mongo Docker Compose Config 37 | mongo: 38 | container_name: mongo 39 | image: mongo:4.4.14-rc0-focal 40 | restart: always 41 | ports: 42 | - "27017:27017" 43 | expose: 44 | - "27017" 45 | volumes: 46 | - ./mongo-data:/data/db 47 | 48 | ## Keycloak Config with Mysql database 49 | keycloak-mysql: 50 | container_name: keycloak-mysql 51 | image: mysql:5.7 52 | volumes: 53 | - ./mysql_keycloak_data:/var/lib/mysql 54 | environment: 55 | MYSQL_ROOT_PASSWORD: root 56 | MYSQL_DATABASE: keycloak 57 | MYSQL_USER: keycloak 58 | MYSQL_PASSWORD: password 59 | 60 | keycloak: 61 | container_name: keycloak 62 | image: quay.io/keycloak/keycloak:18.0.0 63 | command: [ "start-dev", "--import-realm" ] 64 | environment: 65 | DB_VENDOR: MYSQL 66 | DB_ADDR: mysql 67 | DB_DATABASE: keycloak 68 | DB_USER: keycloak 69 | DB_PASSWORD: password 70 | KEYCLOAK_ADMIN: admin 71 | KEYCLOAK_ADMIN_PASSWORD: admin 72 | ports: 73 | - "8080:8080" 74 | volumes: 75 | - ./realms/:/opt/keycloak/data/import/ 76 | depends_on: 77 | - keycloak-mysql 78 | 79 | zookeeper: 80 | image: confluentinc/cp-zookeeper:7.0.1 81 | container_name: zookeeper 82 | ports: 83 | - "2181:2181" 84 | environment: 85 | ZOOKEEPER_CLIENT_PORT: 2181 86 | ZOOKEEPER_TICK_TIME: 2000 87 | 88 | broker: 89 | image: confluentinc/cp-kafka:7.0.1 90 | container_name: broker 91 | ports: 92 | - "9092:9092" 93 | depends_on: 94 | - zookeeper 95 | environment: 96 | KAFKA_BROKER_ID: 1 97 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 98 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT 99 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 100 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 101 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 102 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 103 | 104 | ## Zipkin 105 | zipkin: 106 | image: openzipkin/zipkin 107 | container_name: zipkin 108 | ports: 109 | - "9411:9411" 110 | 111 | ## Eureka Server 112 | discovery-server: 113 | image: microservices-tutorial/discovery-server:latest 114 | container_name: discovery-server 115 | ports: 116 | - "8761:8761" 117 | environment: 118 | - SPRING_PROFILES_ACTIVE=docker 119 | depends_on: 120 | - zipkin 121 | 122 | api-gateway: 123 | image: microservices-tutorial/api-gateway:latest 124 | container_name: api-gateway 125 | ports: 126 | - "8181:8080" 127 | expose: 128 | - "8181" 129 | environment: 130 | - SPRING_PROFILES_ACTIVE=docker 131 | - LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_SECURITY= TRACE 132 | depends_on: 133 | - zipkin 134 | - discovery-server 135 | - keycloak 136 | 137 | ## Product-Service Docker Compose Config 138 | product-service: 139 | container_name: product-service 140 | image: microservices-tutorial/product-service:latest 141 | environment: 142 | - SPRING_PROFILES_ACTIVE=docker 143 | depends_on: 144 | - mongo 145 | - discovery-server 146 | - api-gateway 147 | 148 | ## Order-Service Docker Compose Config 149 | order-service: 150 | container_name: order-service 151 | image: microservices-tutorial/order-service:latest 152 | environment: 153 | - SPRING_PROFILES_ACTIVE=docker 154 | - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-order:5431/order-service 155 | depends_on: 156 | - postgres-order 157 | - broker 158 | - zipkin 159 | - discovery-server 160 | - api-gateway 161 | 162 | ## Inventory-Service Docker Compose Config 163 | inventory-service: 164 | container_name: inventory-service 165 | image: microservices-tutorial/inventory-service:latest 166 | environment: 167 | - SPRING_PROFILES_ACTIVE=docker 168 | - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-inventory:5432/inventory-service 169 | depends_on: 170 | - postgres-inventory 171 | - discovery-server 172 | - api-gateway 173 | 174 | ## Notification-Service Docker Compose Config 175 | notification-service: 176 | container_name: notification-service 177 | image: microservices-tutorial/notification-service:latest 178 | environment: 179 | - SPRING_PROFILES_ACTIVE=docker 180 | depends_on: 181 | - zipkin 182 | - broker 183 | - discovery-server 184 | - api-gateway 185 | 186 | ## Prometheus 187 | prometheus: 188 | image: prom/prometheus:v2.37.1 189 | container_name: prometheus 190 | restart: always 191 | ports: 192 | - "9090:9090" 193 | volumes: 194 | - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 195 | depends_on: 196 | - product-service 197 | - inventory-service 198 | - order-service 199 | - notification-service 200 | 201 | grafana: 202 | image: grafana/grafana-oss:8.5.2 203 | container_name: grafana 204 | restart: always 205 | ports: 206 | - "3000:3000" 207 | links: 208 | - prometheus:prometheus 209 | volumes: 210 | - ./grafana:/var/lib/grafana 211 | environment: 212 | - GF_SECURITY_ADMIN_USER=admin 213 | - GF_SECURITY_ADMIN_PASSWORD=password 214 | -------------------------------------------------------------------------------- /realms/realm-export.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "spring-boot-microservices-realm", 3 | "realm": "spring-boot-microservices-realm", 4 | "notBefore": 0, 5 | "defaultSignatureAlgorithm": "RS256", 6 | "revokeRefreshToken": false, 7 | "refreshTokenMaxReuse": 0, 8 | "accessTokenLifespan": 300, 9 | "accessTokenLifespanForImplicitFlow": 900, 10 | "ssoSessionIdleTimeout": 1800, 11 | "ssoSessionMaxLifespan": 36000, 12 | "ssoSessionIdleTimeoutRememberMe": 0, 13 | "ssoSessionMaxLifespanRememberMe": 0, 14 | "offlineSessionIdleTimeout": 2592000, 15 | "offlineSessionMaxLifespanEnabled": false, 16 | "offlineSessionMaxLifespan": 5184000, 17 | "clientSessionIdleTimeout": 0, 18 | "clientSessionMaxLifespan": 0, 19 | "clientOfflineSessionIdleTimeout": 0, 20 | "clientOfflineSessionMaxLifespan": 0, 21 | "accessCodeLifespan": 60, 22 | "accessCodeLifespanUserAction": 300, 23 | "accessCodeLifespanLogin": 1800, 24 | "actionTokenGeneratedByAdminLifespan": 43200, 25 | "actionTokenGeneratedByUserLifespan": 300, 26 | "oauth2DeviceCodeLifespan": 600, 27 | "oauth2DevicePollingInterval": 5, 28 | "enabled": true, 29 | "sslRequired": "external", 30 | "registrationAllowed": false, 31 | "registrationEmailAsUsername": false, 32 | "rememberMe": false, 33 | "verifyEmail": false, 34 | "loginWithEmailAllowed": true, 35 | "duplicateEmailsAllowed": false, 36 | "resetPasswordAllowed": false, 37 | "editUsernameAllowed": false, 38 | "bruteForceProtected": false, 39 | "permanentLockout": false, 40 | "maxFailureWaitSeconds": 900, 41 | "minimumQuickLoginWaitSeconds": 60, 42 | "waitIncrementSeconds": 60, 43 | "quickLoginCheckMilliSeconds": 1000, 44 | "maxDeltaTimeSeconds": 43200, 45 | "failureFactor": 30, 46 | "defaultRole": { 47 | "id": "22cc1145-79bb-4479-9a9a-f736cf9c1dea", 48 | "name": "default-roles-spring-boot-microservices-realm", 49 | "description": "${role_default-roles}", 50 | "composite": true, 51 | "clientRole": false, 52 | "containerId": "spring-boot-microservices-realm" 53 | }, 54 | "requiredCredentials": [ 55 | "password" 56 | ], 57 | "otpPolicyType": "totp", 58 | "otpPolicyAlgorithm": "HmacSHA1", 59 | "otpPolicyInitialCounter": 0, 60 | "otpPolicyDigits": 6, 61 | "otpPolicyLookAheadWindow": 1, 62 | "otpPolicyPeriod": 30, 63 | "otpSupportedApplications": [ 64 | "FreeOTP", 65 | "Google Authenticator" 66 | ], 67 | "webAuthnPolicyRpEntityName": "keycloak", 68 | "webAuthnPolicySignatureAlgorithms": [ 69 | "ES256" 70 | ], 71 | "webAuthnPolicyRpId": "", 72 | "webAuthnPolicyAttestationConveyancePreference": "not specified", 73 | "webAuthnPolicyAuthenticatorAttachment": "not specified", 74 | "webAuthnPolicyRequireResidentKey": "not specified", 75 | "webAuthnPolicyUserVerificationRequirement": "not specified", 76 | "webAuthnPolicyCreateTimeout": 0, 77 | "webAuthnPolicyAvoidSameAuthenticatorRegister": false, 78 | "webAuthnPolicyAcceptableAaguids": [], 79 | "webAuthnPolicyPasswordlessRpEntityName": "keycloak", 80 | "webAuthnPolicyPasswordlessSignatureAlgorithms": [ 81 | "ES256" 82 | ], 83 | "webAuthnPolicyPasswordlessRpId": "", 84 | "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", 85 | "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", 86 | "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", 87 | "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", 88 | "webAuthnPolicyPasswordlessCreateTimeout": 0, 89 | "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, 90 | "webAuthnPolicyPasswordlessAcceptableAaguids": [], 91 | "users": [ 92 | { 93 | "id": "eeb3f34b-4ccd-44c0-9250-a76afec62961", 94 | "createdTimestamp": 1651438240319, 95 | "username": "service-account-spring-cloud-client", 96 | "enabled": true, 97 | "totp": false, 98 | "emailVerified": false, 99 | "serviceAccountClientId": "spring-cloud-client", 100 | "disableableCredentialTypes": [], 101 | "requiredActions": [], 102 | "notBefore": 0 103 | } 104 | ], 105 | "scopeMappings": [ 106 | { 107 | "clientScope": "offline_access", 108 | "roles": [ 109 | "offline_access" 110 | ] 111 | } 112 | ], 113 | "clientScopeMappings": { 114 | "account": [ 115 | { 116 | "client": "account-console", 117 | "roles": [ 118 | "manage-account" 119 | ] 120 | } 121 | ] 122 | }, 123 | "clients": [ 124 | { 125 | "id": "88330316-990d-49f4-a369-495cbfd30288", 126 | "clientId": "account", 127 | "name": "${client_account}", 128 | "rootUrl": "${authBaseUrl}", 129 | "baseUrl": "/realms/spring-boot-microservices-realm/account/", 130 | "surrogateAuthRequired": false, 131 | "enabled": true, 132 | "alwaysDisplayInConsole": false, 133 | "clientAuthenticatorType": "client-secret", 134 | "redirectUris": [ 135 | "/realms/spring-boot-microservices-realm/account/*" 136 | ], 137 | "webOrigins": [], 138 | "notBefore": 0, 139 | "bearerOnly": false, 140 | "consentRequired": false, 141 | "standardFlowEnabled": true, 142 | "implicitFlowEnabled": false, 143 | "directAccessGrantsEnabled": false, 144 | "serviceAccountsEnabled": false, 145 | "publicClient": true, 146 | "frontchannelLogout": false, 147 | "protocol": "openid-connect", 148 | "attributes": {}, 149 | "authenticationFlowBindingOverrides": {}, 150 | "fullScopeAllowed": false, 151 | "nodeReRegistrationTimeout": 0, 152 | "defaultClientScopes": [ 153 | "web-origins", 154 | "acr", 155 | "roles", 156 | "profile", 157 | "email" 158 | ], 159 | "optionalClientScopes": [ 160 | "address", 161 | "phone", 162 | "offline_access", 163 | "microprofile-jwt" 164 | ] 165 | }, 166 | { 167 | "id": "0df72f71-c8ae-4a03-b0db-8e1e6200402a", 168 | "clientId": "account-console", 169 | "name": "${client_account-console}", 170 | "rootUrl": "${authBaseUrl}", 171 | "baseUrl": "/realms/spring-boot-microservices-realm/account/", 172 | "surrogateAuthRequired": false, 173 | "enabled": true, 174 | "alwaysDisplayInConsole": false, 175 | "clientAuthenticatorType": "client-secret", 176 | "redirectUris": [ 177 | "/realms/spring-boot-microservices-realm/account/*" 178 | ], 179 | "webOrigins": [], 180 | "notBefore": 0, 181 | "bearerOnly": false, 182 | "consentRequired": false, 183 | "standardFlowEnabled": true, 184 | "implicitFlowEnabled": false, 185 | "directAccessGrantsEnabled": false, 186 | "serviceAccountsEnabled": false, 187 | "publicClient": true, 188 | "frontchannelLogout": false, 189 | "protocol": "openid-connect", 190 | "attributes": { 191 | "pkce.code.challenge.method": "S256" 192 | }, 193 | "authenticationFlowBindingOverrides": {}, 194 | "fullScopeAllowed": false, 195 | "nodeReRegistrationTimeout": 0, 196 | "protocolMappers": [ 197 | { 198 | "id": "84371849-ebfb-4e26-a190-2d8f8eb36d21", 199 | "name": "audience resolve", 200 | "protocol": "openid-connect", 201 | "protocolMapper": "oidc-audience-resolve-mapper", 202 | "consentRequired": false, 203 | "config": {} 204 | } 205 | ], 206 | "defaultClientScopes": [ 207 | "web-origins", 208 | "acr", 209 | "roles", 210 | "profile", 211 | "email" 212 | ], 213 | "optionalClientScopes": [ 214 | "address", 215 | "phone", 216 | "offline_access", 217 | "microprofile-jwt" 218 | ] 219 | }, 220 | { 221 | "id": "5cd99b2b-888c-4069-8a47-1078f18b6d64", 222 | "clientId": "admin-cli", 223 | "name": "${client_admin-cli}", 224 | "surrogateAuthRequired": false, 225 | "enabled": true, 226 | "alwaysDisplayInConsole": false, 227 | "clientAuthenticatorType": "client-secret", 228 | "redirectUris": [], 229 | "webOrigins": [], 230 | "notBefore": 0, 231 | "bearerOnly": false, 232 | "consentRequired": false, 233 | "standardFlowEnabled": false, 234 | "implicitFlowEnabled": false, 235 | "directAccessGrantsEnabled": true, 236 | "serviceAccountsEnabled": false, 237 | "publicClient": true, 238 | "frontchannelLogout": false, 239 | "protocol": "openid-connect", 240 | "attributes": {}, 241 | "authenticationFlowBindingOverrides": {}, 242 | "fullScopeAllowed": false, 243 | "nodeReRegistrationTimeout": 0, 244 | "defaultClientScopes": [ 245 | "web-origins", 246 | "acr", 247 | "roles", 248 | "profile", 249 | "email" 250 | ], 251 | "optionalClientScopes": [ 252 | "address", 253 | "phone", 254 | "offline_access", 255 | "microprofile-jwt" 256 | ] 257 | }, 258 | { 259 | "id": "81263d9c-7a87-4fc5-97ee-aae628485fb8", 260 | "clientId": "broker", 261 | "name": "${client_broker}", 262 | "surrogateAuthRequired": false, 263 | "enabled": true, 264 | "alwaysDisplayInConsole": false, 265 | "clientAuthenticatorType": "client-secret", 266 | "redirectUris": [], 267 | "webOrigins": [], 268 | "notBefore": 0, 269 | "bearerOnly": true, 270 | "consentRequired": false, 271 | "standardFlowEnabled": true, 272 | "implicitFlowEnabled": false, 273 | "directAccessGrantsEnabled": false, 274 | "serviceAccountsEnabled": false, 275 | "publicClient": false, 276 | "frontchannelLogout": false, 277 | "protocol": "openid-connect", 278 | "attributes": {}, 279 | "authenticationFlowBindingOverrides": {}, 280 | "fullScopeAllowed": false, 281 | "nodeReRegistrationTimeout": 0, 282 | "defaultClientScopes": [ 283 | "web-origins", 284 | "acr", 285 | "roles", 286 | "profile", 287 | "email" 288 | ], 289 | "optionalClientScopes": [ 290 | "address", 291 | "phone", 292 | "offline_access", 293 | "microprofile-jwt" 294 | ] 295 | }, 296 | { 297 | "id": "4e70b2d6-0ad7-4af2-adf6-7dea3ea3e40d", 298 | "clientId": "realm-management", 299 | "name": "${client_realm-management}", 300 | "surrogateAuthRequired": false, 301 | "enabled": true, 302 | "alwaysDisplayInConsole": false, 303 | "clientAuthenticatorType": "client-secret", 304 | "redirectUris": [], 305 | "webOrigins": [], 306 | "notBefore": 0, 307 | "bearerOnly": true, 308 | "consentRequired": false, 309 | "standardFlowEnabled": true, 310 | "implicitFlowEnabled": false, 311 | "directAccessGrantsEnabled": false, 312 | "serviceAccountsEnabled": false, 313 | "publicClient": false, 314 | "frontchannelLogout": false, 315 | "protocol": "openid-connect", 316 | "attributes": {}, 317 | "authenticationFlowBindingOverrides": {}, 318 | "fullScopeAllowed": false, 319 | "nodeReRegistrationTimeout": 0, 320 | "defaultClientScopes": [ 321 | "web-origins", 322 | "acr", 323 | "roles", 324 | "profile", 325 | "email" 326 | ], 327 | "optionalClientScopes": [ 328 | "address", 329 | "phone", 330 | "offline_access", 331 | "microprofile-jwt" 332 | ] 333 | }, 334 | { 335 | "id": "9aac314d-c3bc-4fd5-b182-0aaffbebeb53", 336 | "clientId": "security-admin-console", 337 | "name": "${client_security-admin-console}", 338 | "rootUrl": "${authAdminUrl}", 339 | "baseUrl": "/admin/spring-boot-microservices-realm/console/", 340 | "surrogateAuthRequired": false, 341 | "enabled": true, 342 | "alwaysDisplayInConsole": false, 343 | "clientAuthenticatorType": "client-secret", 344 | "redirectUris": [ 345 | "/admin/spring-boot-microservices-realm/console/*" 346 | ], 347 | "webOrigins": [ 348 | "+" 349 | ], 350 | "notBefore": 0, 351 | "bearerOnly": false, 352 | "consentRequired": false, 353 | "standardFlowEnabled": true, 354 | "implicitFlowEnabled": false, 355 | "directAccessGrantsEnabled": false, 356 | "serviceAccountsEnabled": false, 357 | "publicClient": true, 358 | "frontchannelLogout": false, 359 | "protocol": "openid-connect", 360 | "attributes": { 361 | "pkce.code.challenge.method": "S256" 362 | }, 363 | "authenticationFlowBindingOverrides": {}, 364 | "fullScopeAllowed": false, 365 | "nodeReRegistrationTimeout": 0, 366 | "protocolMappers": [ 367 | { 368 | "id": "43ddad03-b28f-4015-b146-20c6508688d6", 369 | "name": "locale", 370 | "protocol": "openid-connect", 371 | "protocolMapper": "oidc-usermodel-attribute-mapper", 372 | "consentRequired": false, 373 | "config": { 374 | "userinfo.token.claim": "true", 375 | "user.attribute": "locale", 376 | "id.token.claim": "true", 377 | "access.token.claim": "true", 378 | "claim.name": "locale", 379 | "jsonType.label": "String" 380 | } 381 | } 382 | ], 383 | "defaultClientScopes": [ 384 | "web-origins", 385 | "acr", 386 | "roles", 387 | "profile", 388 | "email" 389 | ], 390 | "optionalClientScopes": [ 391 | "address", 392 | "phone", 393 | "offline_access", 394 | "microprofile-jwt" 395 | ] 396 | }, 397 | { 398 | "id": "97c3b790-0cfc-4c78-8ed7-c31ab23f9b56", 399 | "clientId": "spring-cloud-client", 400 | "surrogateAuthRequired": false, 401 | "enabled": true, 402 | "alwaysDisplayInConsole": false, 403 | "clientAuthenticatorType": "client-secret", 404 | "secret": "**********", 405 | "redirectUris": [], 406 | "webOrigins": [], 407 | "notBefore": 0, 408 | "bearerOnly": false, 409 | "consentRequired": false, 410 | "standardFlowEnabled": false, 411 | "implicitFlowEnabled": false, 412 | "directAccessGrantsEnabled": false, 413 | "serviceAccountsEnabled": true, 414 | "publicClient": false, 415 | "frontchannelLogout": false, 416 | "protocol": "openid-connect", 417 | "attributes": { 418 | "saml.force.post.binding": "false", 419 | "saml.multivalued.roles": "false", 420 | "frontchannel.logout.session.required": "false", 421 | "oauth2.device.authorization.grant.enabled": "false", 422 | "backchannel.logout.revoke.offline.tokens": "false", 423 | "saml.server.signature.keyinfo.ext": "false", 424 | "use.refresh.tokens": "true", 425 | "oidc.ciba.grant.enabled": "false", 426 | "backchannel.logout.session.required": "true", 427 | "client_credentials.use_refresh_token": "false", 428 | "require.pushed.authorization.requests": "false", 429 | "saml.client.signature": "false", 430 | "saml.allow.ecp.flow": "false", 431 | "id.token.as.detached.signature": "false", 432 | "saml.assertion.signature": "false", 433 | "client.secret.creation.time": "1651438240", 434 | "saml.encrypt": "false", 435 | "saml.server.signature": "false", 436 | "exclude.session.state.from.auth.response": "false", 437 | "saml.artifact.binding": "false", 438 | "saml_force_name_id_format": "false", 439 | "acr.loa.map": "{}", 440 | "tls.client.certificate.bound.access.tokens": "false", 441 | "saml.authnstatement": "false", 442 | "display.on.consent.screen": "false", 443 | "token.response.type.bearer.lower-case": "false", 444 | "saml.onetimeuse.condition": "false" 445 | }, 446 | "authenticationFlowBindingOverrides": {}, 447 | "fullScopeAllowed": true, 448 | "nodeReRegistrationTimeout": -1, 449 | "protocolMappers": [ 450 | { 451 | "id": "e7872e20-fead-4c7c-ac02-5a9c6c033d9e", 452 | "name": "Client ID", 453 | "protocol": "openid-connect", 454 | "protocolMapper": "oidc-usersessionmodel-note-mapper", 455 | "consentRequired": false, 456 | "config": { 457 | "user.session.note": "clientId", 458 | "id.token.claim": "true", 459 | "access.token.claim": "true", 460 | "claim.name": "clientId", 461 | "jsonType.label": "String" 462 | } 463 | }, 464 | { 465 | "id": "4d0b732c-dac6-4594-ad1c-ce18e1b18495", 466 | "name": "Client IP Address", 467 | "protocol": "openid-connect", 468 | "protocolMapper": "oidc-usersessionmodel-note-mapper", 469 | "consentRequired": false, 470 | "config": { 471 | "user.session.note": "clientAddress", 472 | "id.token.claim": "true", 473 | "access.token.claim": "true", 474 | "claim.name": "clientAddress", 475 | "jsonType.label": "String" 476 | } 477 | }, 478 | { 479 | "id": "3a68cc51-59fa-4a7f-8fa3-a2ddcb592c7c", 480 | "name": "Client Host", 481 | "protocol": "openid-connect", 482 | "protocolMapper": "oidc-usersessionmodel-note-mapper", 483 | "consentRequired": false, 484 | "config": { 485 | "user.session.note": "clientHost", 486 | "id.token.claim": "true", 487 | "access.token.claim": "true", 488 | "claim.name": "clientHost", 489 | "jsonType.label": "String" 490 | } 491 | } 492 | ], 493 | "defaultClientScopes": [ 494 | "web-origins", 495 | "acr", 496 | "roles", 497 | "profile", 498 | "email" 499 | ], 500 | "optionalClientScopes": [ 501 | "address", 502 | "phone", 503 | "offline_access", 504 | "microprofile-jwt" 505 | ] 506 | } 507 | ], 508 | "clientScopes": [ 509 | { 510 | "id": "2733b63d-8173-4a31-a2ca-238e901e78c6", 511 | "name": "roles", 512 | "description": "OpenID Connect scope for add user roles to the access token", 513 | "protocol": "openid-connect", 514 | "attributes": { 515 | "include.in.token.scope": "false", 516 | "display.on.consent.screen": "true", 517 | "consent.screen.text": "${rolesScopeConsentText}" 518 | }, 519 | "protocolMappers": [ 520 | { 521 | "id": "d5b7fd54-95b7-4cdd-a750-5a11726ac18e", 522 | "name": "realm roles", 523 | "protocol": "openid-connect", 524 | "protocolMapper": "oidc-usermodel-realm-role-mapper", 525 | "consentRequired": false, 526 | "config": { 527 | "user.attribute": "foo", 528 | "access.token.claim": "true", 529 | "claim.name": "realm_access.roles", 530 | "jsonType.label": "String", 531 | "multivalued": "true" 532 | } 533 | }, 534 | { 535 | "id": "963c2f99-cfe5-47cc-b469-97ca87afc18e", 536 | "name": "audience resolve", 537 | "protocol": "openid-connect", 538 | "protocolMapper": "oidc-audience-resolve-mapper", 539 | "consentRequired": false, 540 | "config": {} 541 | }, 542 | { 543 | "id": "56f09162-66b5-42c0-91a8-67f97356baa0", 544 | "name": "client roles", 545 | "protocol": "openid-connect", 546 | "protocolMapper": "oidc-usermodel-client-role-mapper", 547 | "consentRequired": false, 548 | "config": { 549 | "user.attribute": "foo", 550 | "access.token.claim": "true", 551 | "claim.name": "resource_access.${client_id}.roles", 552 | "jsonType.label": "String", 553 | "multivalued": "true" 554 | } 555 | } 556 | ] 557 | }, 558 | { 559 | "id": "67f93bde-f22b-4c88-a8ab-1c034514b61a", 560 | "name": "profile", 561 | "description": "OpenID Connect built-in scope: profile", 562 | "protocol": "openid-connect", 563 | "attributes": { 564 | "include.in.token.scope": "true", 565 | "display.on.consent.screen": "true", 566 | "consent.screen.text": "${profileScopeConsentText}" 567 | }, 568 | "protocolMappers": [ 569 | { 570 | "id": "32e2cee0-13c7-45bd-89f6-3a3e64b30ef6", 571 | "name": "updated at", 572 | "protocol": "openid-connect", 573 | "protocolMapper": "oidc-usermodel-attribute-mapper", 574 | "consentRequired": false, 575 | "config": { 576 | "userinfo.token.claim": "true", 577 | "user.attribute": "updatedAt", 578 | "id.token.claim": "true", 579 | "access.token.claim": "true", 580 | "claim.name": "updated_at", 581 | "jsonType.label": "long" 582 | } 583 | }, 584 | { 585 | "id": "6609d9cf-691a-4166-8dff-921f9040b2d4", 586 | "name": "website", 587 | "protocol": "openid-connect", 588 | "protocolMapper": "oidc-usermodel-attribute-mapper", 589 | "consentRequired": false, 590 | "config": { 591 | "userinfo.token.claim": "true", 592 | "user.attribute": "website", 593 | "id.token.claim": "true", 594 | "access.token.claim": "true", 595 | "claim.name": "website", 596 | "jsonType.label": "String" 597 | } 598 | }, 599 | { 600 | "id": "1106ffa8-63f4-4d81-b630-88ecad969a33", 601 | "name": "picture", 602 | "protocol": "openid-connect", 603 | "protocolMapper": "oidc-usermodel-attribute-mapper", 604 | "consentRequired": false, 605 | "config": { 606 | "userinfo.token.claim": "true", 607 | "user.attribute": "picture", 608 | "id.token.claim": "true", 609 | "access.token.claim": "true", 610 | "claim.name": "picture", 611 | "jsonType.label": "String" 612 | } 613 | }, 614 | { 615 | "id": "64b038f4-5f5b-41f2-85fb-eee85e330805", 616 | "name": "given name", 617 | "protocol": "openid-connect", 618 | "protocolMapper": "oidc-usermodel-property-mapper", 619 | "consentRequired": false, 620 | "config": { 621 | "userinfo.token.claim": "true", 622 | "user.attribute": "firstName", 623 | "id.token.claim": "true", 624 | "access.token.claim": "true", 625 | "claim.name": "given_name", 626 | "jsonType.label": "String" 627 | } 628 | }, 629 | { 630 | "id": "57d99f91-e7f9-4920-bb65-418129343365", 631 | "name": "locale", 632 | "protocol": "openid-connect", 633 | "protocolMapper": "oidc-usermodel-attribute-mapper", 634 | "consentRequired": false, 635 | "config": { 636 | "userinfo.token.claim": "true", 637 | "user.attribute": "locale", 638 | "id.token.claim": "true", 639 | "access.token.claim": "true", 640 | "claim.name": "locale", 641 | "jsonType.label": "String" 642 | } 643 | }, 644 | { 645 | "id": "65d8073d-80dd-444c-881b-231fca55a822", 646 | "name": "family name", 647 | "protocol": "openid-connect", 648 | "protocolMapper": "oidc-usermodel-property-mapper", 649 | "consentRequired": false, 650 | "config": { 651 | "userinfo.token.claim": "true", 652 | "user.attribute": "lastName", 653 | "id.token.claim": "true", 654 | "access.token.claim": "true", 655 | "claim.name": "family_name", 656 | "jsonType.label": "String" 657 | } 658 | }, 659 | { 660 | "id": "004b5dd2-3958-4f6d-b46e-6150f338b9cc", 661 | "name": "gender", 662 | "protocol": "openid-connect", 663 | "protocolMapper": "oidc-usermodel-attribute-mapper", 664 | "consentRequired": false, 665 | "config": { 666 | "userinfo.token.claim": "true", 667 | "user.attribute": "gender", 668 | "id.token.claim": "true", 669 | "access.token.claim": "true", 670 | "claim.name": "gender", 671 | "jsonType.label": "String" 672 | } 673 | }, 674 | { 675 | "id": "730ac25a-9db2-4bf5-9ec7-215453af3864", 676 | "name": "profile", 677 | "protocol": "openid-connect", 678 | "protocolMapper": "oidc-usermodel-attribute-mapper", 679 | "consentRequired": false, 680 | "config": { 681 | "userinfo.token.claim": "true", 682 | "user.attribute": "profile", 683 | "id.token.claim": "true", 684 | "access.token.claim": "true", 685 | "claim.name": "profile", 686 | "jsonType.label": "String" 687 | } 688 | }, 689 | { 690 | "id": "3e7884b4-8e0e-4b13-ad36-ef4ae05339bb", 691 | "name": "zoneinfo", 692 | "protocol": "openid-connect", 693 | "protocolMapper": "oidc-usermodel-attribute-mapper", 694 | "consentRequired": false, 695 | "config": { 696 | "userinfo.token.claim": "true", 697 | "user.attribute": "zoneinfo", 698 | "id.token.claim": "true", 699 | "access.token.claim": "true", 700 | "claim.name": "zoneinfo", 701 | "jsonType.label": "String" 702 | } 703 | }, 704 | { 705 | "id": "d83ed0ac-7448-46e1-ba70-ffd88f2a2850", 706 | "name": "birthdate", 707 | "protocol": "openid-connect", 708 | "protocolMapper": "oidc-usermodel-attribute-mapper", 709 | "consentRequired": false, 710 | "config": { 711 | "userinfo.token.claim": "true", 712 | "user.attribute": "birthdate", 713 | "id.token.claim": "true", 714 | "access.token.claim": "true", 715 | "claim.name": "birthdate", 716 | "jsonType.label": "String" 717 | } 718 | }, 719 | { 720 | "id": "0007cac9-ea15-4e5b-b9a9-8a802fc1f748", 721 | "name": "full name", 722 | "protocol": "openid-connect", 723 | "protocolMapper": "oidc-full-name-mapper", 724 | "consentRequired": false, 725 | "config": { 726 | "id.token.claim": "true", 727 | "access.token.claim": "true", 728 | "userinfo.token.claim": "true" 729 | } 730 | }, 731 | { 732 | "id": "5c754ec7-7859-4014-a620-9ff418850253", 733 | "name": "middle name", 734 | "protocol": "openid-connect", 735 | "protocolMapper": "oidc-usermodel-attribute-mapper", 736 | "consentRequired": false, 737 | "config": { 738 | "userinfo.token.claim": "true", 739 | "user.attribute": "middleName", 740 | "id.token.claim": "true", 741 | "access.token.claim": "true", 742 | "claim.name": "middle_name", 743 | "jsonType.label": "String" 744 | } 745 | }, 746 | { 747 | "id": "82a2689a-afe0-43da-84c2-a0c357799c25", 748 | "name": "username", 749 | "protocol": "openid-connect", 750 | "protocolMapper": "oidc-usermodel-property-mapper", 751 | "consentRequired": false, 752 | "config": { 753 | "userinfo.token.claim": "true", 754 | "user.attribute": "username", 755 | "id.token.claim": "true", 756 | "access.token.claim": "true", 757 | "claim.name": "preferred_username", 758 | "jsonType.label": "String" 759 | } 760 | }, 761 | { 762 | "id": "77c75db9-ca22-4a6f-a62e-8ad02192e420", 763 | "name": "nickname", 764 | "protocol": "openid-connect", 765 | "protocolMapper": "oidc-usermodel-attribute-mapper", 766 | "consentRequired": false, 767 | "config": { 768 | "userinfo.token.claim": "true", 769 | "user.attribute": "nickname", 770 | "id.token.claim": "true", 771 | "access.token.claim": "true", 772 | "claim.name": "nickname", 773 | "jsonType.label": "String" 774 | } 775 | } 776 | ] 777 | }, 778 | { 779 | "id": "74e67ea9-3438-40af-bf97-6ef8e9733a1e", 780 | "name": "web-origins", 781 | "description": "OpenID Connect scope for add allowed web origins to the access token", 782 | "protocol": "openid-connect", 783 | "attributes": { 784 | "include.in.token.scope": "false", 785 | "display.on.consent.screen": "false", 786 | "consent.screen.text": "" 787 | }, 788 | "protocolMappers": [ 789 | { 790 | "id": "3c149e93-db9a-40f5-ac00-b4c3a28d38b8", 791 | "name": "allowed web origins", 792 | "protocol": "openid-connect", 793 | "protocolMapper": "oidc-allowed-origins-mapper", 794 | "consentRequired": false, 795 | "config": {} 796 | } 797 | ] 798 | }, 799 | { 800 | "id": "202f6b20-9823-4b6b-973f-9536b2b46565", 801 | "name": "offline_access", 802 | "description": "OpenID Connect built-in scope: offline_access", 803 | "protocol": "openid-connect", 804 | "attributes": { 805 | "consent.screen.text": "${offlineAccessScopeConsentText}", 806 | "display.on.consent.screen": "true" 807 | } 808 | }, 809 | { 810 | "id": "e31817cc-17ab-4c37-8c45-f2aaf58f6bd8", 811 | "name": "email", 812 | "description": "OpenID Connect built-in scope: email", 813 | "protocol": "openid-connect", 814 | "attributes": { 815 | "include.in.token.scope": "true", 816 | "display.on.consent.screen": "true", 817 | "consent.screen.text": "${emailScopeConsentText}" 818 | }, 819 | "protocolMappers": [ 820 | { 821 | "id": "6a23790d-9f64-41c2-886c-2d9426a8a623", 822 | "name": "email verified", 823 | "protocol": "openid-connect", 824 | "protocolMapper": "oidc-usermodel-property-mapper", 825 | "consentRequired": false, 826 | "config": { 827 | "userinfo.token.claim": "true", 828 | "user.attribute": "emailVerified", 829 | "id.token.claim": "true", 830 | "access.token.claim": "true", 831 | "claim.name": "email_verified", 832 | "jsonType.label": "boolean" 833 | } 834 | }, 835 | { 836 | "id": "1a1c68f0-8b61-4660-9010-e85f57904504", 837 | "name": "email", 838 | "protocol": "openid-connect", 839 | "protocolMapper": "oidc-usermodel-property-mapper", 840 | "consentRequired": false, 841 | "config": { 842 | "userinfo.token.claim": "true", 843 | "user.attribute": "email", 844 | "id.token.claim": "true", 845 | "access.token.claim": "true", 846 | "claim.name": "email", 847 | "jsonType.label": "String" 848 | } 849 | } 850 | ] 851 | }, 852 | { 853 | "id": "a5b827a1-d9f3-4d72-9155-96c76e9d1455", 854 | "name": "phone", 855 | "description": "OpenID Connect built-in scope: phone", 856 | "protocol": "openid-connect", 857 | "attributes": { 858 | "include.in.token.scope": "true", 859 | "display.on.consent.screen": "true", 860 | "consent.screen.text": "${phoneScopeConsentText}" 861 | }, 862 | "protocolMappers": [ 863 | { 864 | "id": "bd168de1-5366-4637-968e-197252d5d3ca", 865 | "name": "phone number", 866 | "protocol": "openid-connect", 867 | "protocolMapper": "oidc-usermodel-attribute-mapper", 868 | "consentRequired": false, 869 | "config": { 870 | "userinfo.token.claim": "true", 871 | "user.attribute": "phoneNumber", 872 | "id.token.claim": "true", 873 | "access.token.claim": "true", 874 | "claim.name": "phone_number", 875 | "jsonType.label": "String" 876 | } 877 | }, 878 | { 879 | "id": "98905ab9-e211-48e0-8224-290c2fa7ae61", 880 | "name": "phone number verified", 881 | "protocol": "openid-connect", 882 | "protocolMapper": "oidc-usermodel-attribute-mapper", 883 | "consentRequired": false, 884 | "config": { 885 | "userinfo.token.claim": "true", 886 | "user.attribute": "phoneNumberVerified", 887 | "id.token.claim": "true", 888 | "access.token.claim": "true", 889 | "claim.name": "phone_number_verified", 890 | "jsonType.label": "boolean" 891 | } 892 | } 893 | ] 894 | }, 895 | { 896 | "id": "92f09301-fce8-42d7-997f-44b52625c5d0", 897 | "name": "address", 898 | "description": "OpenID Connect built-in scope: address", 899 | "protocol": "openid-connect", 900 | "attributes": { 901 | "include.in.token.scope": "true", 902 | "display.on.consent.screen": "true", 903 | "consent.screen.text": "${addressScopeConsentText}" 904 | }, 905 | "protocolMappers": [ 906 | { 907 | "id": "50eb4bc5-053f-4fa6-abc6-abcaadf1cfa6", 908 | "name": "address", 909 | "protocol": "openid-connect", 910 | "protocolMapper": "oidc-address-mapper", 911 | "consentRequired": false, 912 | "config": { 913 | "user.attribute.formatted": "formatted", 914 | "user.attribute.country": "country", 915 | "user.attribute.postal_code": "postal_code", 916 | "userinfo.token.claim": "true", 917 | "user.attribute.street": "street", 918 | "id.token.claim": "true", 919 | "user.attribute.region": "region", 920 | "access.token.claim": "true", 921 | "user.attribute.locality": "locality" 922 | } 923 | } 924 | ] 925 | }, 926 | { 927 | "id": "835bdff9-1c67-47d1-bc9a-68d6427c265b", 928 | "name": "microprofile-jwt", 929 | "description": "Microprofile - JWT built-in scope", 930 | "protocol": "openid-connect", 931 | "attributes": { 932 | "include.in.token.scope": "true", 933 | "display.on.consent.screen": "false" 934 | }, 935 | "protocolMappers": [ 936 | { 937 | "id": "e9ed2d90-df8b-4c6a-ba2d-eb79d315617d", 938 | "name": "upn", 939 | "protocol": "openid-connect", 940 | "protocolMapper": "oidc-usermodel-property-mapper", 941 | "consentRequired": false, 942 | "config": { 943 | "userinfo.token.claim": "true", 944 | "user.attribute": "username", 945 | "id.token.claim": "true", 946 | "access.token.claim": "true", 947 | "claim.name": "upn", 948 | "jsonType.label": "String" 949 | } 950 | }, 951 | { 952 | "id": "e62e98fa-4599-47a0-a6de-4c6029cc1305", 953 | "name": "groups", 954 | "protocol": "openid-connect", 955 | "protocolMapper": "oidc-usermodel-realm-role-mapper", 956 | "consentRequired": false, 957 | "config": { 958 | "multivalued": "true", 959 | "user.attribute": "foo", 960 | "id.token.claim": "true", 961 | "access.token.claim": "true", 962 | "claim.name": "groups", 963 | "jsonType.label": "String" 964 | } 965 | } 966 | ] 967 | }, 968 | { 969 | "id": "714f075c-afc7-4321-a361-3a38447cfb11", 970 | "name": "acr", 971 | "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", 972 | "protocol": "openid-connect", 973 | "attributes": { 974 | "include.in.token.scope": "false", 975 | "display.on.consent.screen": "false" 976 | }, 977 | "protocolMappers": [ 978 | { 979 | "id": "ebe54999-5b77-4735-8fcb-80197db33299", 980 | "name": "acr loa level", 981 | "protocol": "openid-connect", 982 | "protocolMapper": "oidc-acr-mapper", 983 | "consentRequired": false, 984 | "config": { 985 | "id.token.claim": "true", 986 | "access.token.claim": "true" 987 | } 988 | } 989 | ] 990 | }, 991 | { 992 | "id": "efb37477-5362-4582-8c04-28eba1ba2e2d", 993 | "name": "role_list", 994 | "description": "SAML role list", 995 | "protocol": "saml", 996 | "attributes": { 997 | "consent.screen.text": "${samlRoleListScopeConsentText}", 998 | "display.on.consent.screen": "true" 999 | }, 1000 | "protocolMappers": [ 1001 | { 1002 | "id": "df4a5184-d529-4479-b2fa-f2e4ab651949", 1003 | "name": "role list", 1004 | "protocol": "saml", 1005 | "protocolMapper": "saml-role-list-mapper", 1006 | "consentRequired": false, 1007 | "config": { 1008 | "single": "false", 1009 | "attribute.nameformat": "Basic", 1010 | "attribute.name": "Role" 1011 | } 1012 | } 1013 | ] 1014 | } 1015 | ], 1016 | "defaultDefaultClientScopes": [ 1017 | "roles", 1018 | "profile", 1019 | "acr", 1020 | "web-origins", 1021 | "email", 1022 | "role_list" 1023 | ], 1024 | "defaultOptionalClientScopes": [ 1025 | "offline_access", 1026 | "microprofile-jwt", 1027 | "address", 1028 | "phone" 1029 | ], 1030 | "browserSecurityHeaders": { 1031 | "contentSecurityPolicyReportOnly": "", 1032 | "xContentTypeOptions": "nosniff", 1033 | "xRobotsTag": "none", 1034 | "xFrameOptions": "SAMEORIGIN", 1035 | "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", 1036 | "xXSSProtection": "1; mode=block", 1037 | "strictTransportSecurity": "max-age=31536000; includeSubDomains" 1038 | }, 1039 | "smtpServer": {}, 1040 | "eventsEnabled": false, 1041 | "eventsListeners": [ 1042 | "jboss-logging" 1043 | ], 1044 | "enabledEventTypes": [], 1045 | "adminEventsEnabled": false, 1046 | "adminEventsDetailsEnabled": false, 1047 | "identityProviders": [], 1048 | "identityProviderMappers": [], 1049 | "components": { 1050 | "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ 1051 | { 1052 | "id": "f76b6e22-8b4b-4eb7-afa0-b0c62793640d", 1053 | "name": "Allowed Protocol Mapper Types", 1054 | "providerId": "allowed-protocol-mappers", 1055 | "subType": "authenticated", 1056 | "subComponents": {}, 1057 | "config": { 1058 | "allowed-protocol-mapper-types": [ 1059 | "oidc-full-name-mapper", 1060 | "oidc-sha256-pairwise-sub-mapper", 1061 | "saml-user-property-mapper", 1062 | "oidc-usermodel-property-mapper", 1063 | "oidc-address-mapper", 1064 | "saml-user-attribute-mapper", 1065 | "oidc-usermodel-attribute-mapper", 1066 | "saml-role-list-mapper" 1067 | ] 1068 | } 1069 | }, 1070 | { 1071 | "id": "feb84ff6-a87d-4ea7-b892-6b81f0862d09", 1072 | "name": "Allowed Protocol Mapper Types", 1073 | "providerId": "allowed-protocol-mappers", 1074 | "subType": "anonymous", 1075 | "subComponents": {}, 1076 | "config": { 1077 | "allowed-protocol-mapper-types": [ 1078 | "oidc-full-name-mapper", 1079 | "oidc-address-mapper", 1080 | "oidc-usermodel-attribute-mapper", 1081 | "oidc-sha256-pairwise-sub-mapper", 1082 | "saml-user-attribute-mapper", 1083 | "oidc-usermodel-property-mapper", 1084 | "saml-user-property-mapper", 1085 | "saml-role-list-mapper" 1086 | ] 1087 | } 1088 | }, 1089 | { 1090 | "id": "0f9610c9-2fd8-47b1-b4aa-d28c26e6a51c", 1091 | "name": "Trusted Hosts", 1092 | "providerId": "trusted-hosts", 1093 | "subType": "anonymous", 1094 | "subComponents": {}, 1095 | "config": { 1096 | "host-sending-registration-request-must-match": [ 1097 | "true" 1098 | ], 1099 | "client-uris-must-match": [ 1100 | "true" 1101 | ] 1102 | } 1103 | }, 1104 | { 1105 | "id": "ae0d3b57-56eb-41ca-bf6a-5a31733aaeb7", 1106 | "name": "Max Clients Limit", 1107 | "providerId": "max-clients", 1108 | "subType": "anonymous", 1109 | "subComponents": {}, 1110 | "config": { 1111 | "max-clients": [ 1112 | "200" 1113 | ] 1114 | } 1115 | }, 1116 | { 1117 | "id": "579a13cf-a703-41b5-9678-db3300514122", 1118 | "name": "Allowed Client Scopes", 1119 | "providerId": "allowed-client-templates", 1120 | "subType": "authenticated", 1121 | "subComponents": {}, 1122 | "config": { 1123 | "allow-default-scopes": [ 1124 | "true" 1125 | ] 1126 | } 1127 | }, 1128 | { 1129 | "id": "25ab1a30-8f2e-492d-be6e-6069410a82dd", 1130 | "name": "Full Scope Disabled", 1131 | "providerId": "scope", 1132 | "subType": "anonymous", 1133 | "subComponents": {}, 1134 | "config": {} 1135 | }, 1136 | { 1137 | "id": "c3cecef0-5965-4186-b572-51331c5db2f8", 1138 | "name": "Allowed Client Scopes", 1139 | "providerId": "allowed-client-templates", 1140 | "subType": "anonymous", 1141 | "subComponents": {}, 1142 | "config": { 1143 | "allow-default-scopes": [ 1144 | "true" 1145 | ] 1146 | } 1147 | }, 1148 | { 1149 | "id": "43ea83d8-64c0-4830-b7c7-a3076721f63b", 1150 | "name": "Consent Required", 1151 | "providerId": "consent-required", 1152 | "subType": "anonymous", 1153 | "subComponents": {}, 1154 | "config": {} 1155 | } 1156 | ], 1157 | "org.keycloak.keys.KeyProvider": [ 1158 | { 1159 | "id": "a5bc0a0b-8954-4822-b81f-4df92a3ba5af", 1160 | "name": "rsa-generated", 1161 | "providerId": "rsa-generated", 1162 | "subComponents": {}, 1163 | "config": { 1164 | "priority": [ 1165 | "100" 1166 | ] 1167 | } 1168 | }, 1169 | { 1170 | "id": "52a25f13-0d6d-4915-8655-de3b0454256f", 1171 | "name": "rsa-enc-generated", 1172 | "providerId": "rsa-enc-generated", 1173 | "subComponents": {}, 1174 | "config": { 1175 | "priority": [ 1176 | "100" 1177 | ], 1178 | "algorithm": [ 1179 | "RSA-OAEP" 1180 | ] 1181 | } 1182 | }, 1183 | { 1184 | "id": "8987a85f-3153-4466-a7f8-32e9a141b039", 1185 | "name": "hmac-generated", 1186 | "providerId": "hmac-generated", 1187 | "subComponents": {}, 1188 | "config": { 1189 | "priority": [ 1190 | "100" 1191 | ], 1192 | "algorithm": [ 1193 | "HS256" 1194 | ] 1195 | } 1196 | }, 1197 | { 1198 | "id": "980f2a88-cdc7-403c-b983-721a1792d9e2", 1199 | "name": "aes-generated", 1200 | "providerId": "aes-generated", 1201 | "subComponents": {}, 1202 | "config": { 1203 | "priority": [ 1204 | "100" 1205 | ] 1206 | } 1207 | } 1208 | ] 1209 | }, 1210 | "internationalizationEnabled": false, 1211 | "supportedLocales": [], 1212 | "authenticationFlows": [ 1213 | { 1214 | "id": "d5791a7a-8c15-4344-b6ca-2a8468ddc7d8", 1215 | "alias": "Account verification options", 1216 | "description": "Method with which to verity the existing account", 1217 | "providerId": "basic-flow", 1218 | "topLevel": false, 1219 | "builtIn": true, 1220 | "authenticationExecutions": [ 1221 | { 1222 | "authenticator": "idp-email-verification", 1223 | "authenticatorFlow": false, 1224 | "requirement": "ALTERNATIVE", 1225 | "priority": 10, 1226 | "autheticatorFlow": false, 1227 | "userSetupAllowed": false 1228 | }, 1229 | { 1230 | "authenticatorFlow": true, 1231 | "requirement": "ALTERNATIVE", 1232 | "priority": 20, 1233 | "autheticatorFlow": true, 1234 | "flowAlias": "Verify Existing Account by Re-authentication", 1235 | "userSetupAllowed": false 1236 | } 1237 | ] 1238 | }, 1239 | { 1240 | "id": "74830238-d2fb-4cf7-a12d-397d58b4457d", 1241 | "alias": "Authentication Options", 1242 | "description": "Authentication options.", 1243 | "providerId": "basic-flow", 1244 | "topLevel": false, 1245 | "builtIn": true, 1246 | "authenticationExecutions": [ 1247 | { 1248 | "authenticator": "basic-auth", 1249 | "authenticatorFlow": false, 1250 | "requirement": "REQUIRED", 1251 | "priority": 10, 1252 | "autheticatorFlow": false, 1253 | "userSetupAllowed": false 1254 | }, 1255 | { 1256 | "authenticator": "basic-auth-otp", 1257 | "authenticatorFlow": false, 1258 | "requirement": "DISABLED", 1259 | "priority": 20, 1260 | "autheticatorFlow": false, 1261 | "userSetupAllowed": false 1262 | }, 1263 | { 1264 | "authenticator": "auth-spnego", 1265 | "authenticatorFlow": false, 1266 | "requirement": "DISABLED", 1267 | "priority": 30, 1268 | "autheticatorFlow": false, 1269 | "userSetupAllowed": false 1270 | } 1271 | ] 1272 | }, 1273 | { 1274 | "id": "508924af-d44e-4f2c-8976-898a6ffa3a47", 1275 | "alias": "Browser - Conditional OTP", 1276 | "description": "Flow to determine if the OTP is required for the authentication", 1277 | "providerId": "basic-flow", 1278 | "topLevel": false, 1279 | "builtIn": true, 1280 | "authenticationExecutions": [ 1281 | { 1282 | "authenticator": "conditional-user-configured", 1283 | "authenticatorFlow": false, 1284 | "requirement": "REQUIRED", 1285 | "priority": 10, 1286 | "autheticatorFlow": false, 1287 | "userSetupAllowed": false 1288 | }, 1289 | { 1290 | "authenticator": "auth-otp-form", 1291 | "authenticatorFlow": false, 1292 | "requirement": "REQUIRED", 1293 | "priority": 20, 1294 | "autheticatorFlow": false, 1295 | "userSetupAllowed": false 1296 | } 1297 | ] 1298 | }, 1299 | { 1300 | "id": "cc1fc64e-8192-45a6-95c1-ba1ed34cb1da", 1301 | "alias": "Direct Grant - Conditional OTP", 1302 | "description": "Flow to determine if the OTP is required for the authentication", 1303 | "providerId": "basic-flow", 1304 | "topLevel": false, 1305 | "builtIn": true, 1306 | "authenticationExecutions": [ 1307 | { 1308 | "authenticator": "conditional-user-configured", 1309 | "authenticatorFlow": false, 1310 | "requirement": "REQUIRED", 1311 | "priority": 10, 1312 | "autheticatorFlow": false, 1313 | "userSetupAllowed": false 1314 | }, 1315 | { 1316 | "authenticator": "direct-grant-validate-otp", 1317 | "authenticatorFlow": false, 1318 | "requirement": "REQUIRED", 1319 | "priority": 20, 1320 | "autheticatorFlow": false, 1321 | "userSetupAllowed": false 1322 | } 1323 | ] 1324 | }, 1325 | { 1326 | "id": "592401d6-e9fd-4c5d-b86c-3ebebac5280d", 1327 | "alias": "First broker login - Conditional OTP", 1328 | "description": "Flow to determine if the OTP is required for the authentication", 1329 | "providerId": "basic-flow", 1330 | "topLevel": false, 1331 | "builtIn": true, 1332 | "authenticationExecutions": [ 1333 | { 1334 | "authenticator": "conditional-user-configured", 1335 | "authenticatorFlow": false, 1336 | "requirement": "REQUIRED", 1337 | "priority": 10, 1338 | "autheticatorFlow": false, 1339 | "userSetupAllowed": false 1340 | }, 1341 | { 1342 | "authenticator": "auth-otp-form", 1343 | "authenticatorFlow": false, 1344 | "requirement": "REQUIRED", 1345 | "priority": 20, 1346 | "autheticatorFlow": false, 1347 | "userSetupAllowed": false 1348 | } 1349 | ] 1350 | }, 1351 | { 1352 | "id": "3ede2c6f-9e06-4486-b259-123d15db7c11", 1353 | "alias": "Handle Existing Account", 1354 | "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", 1355 | "providerId": "basic-flow", 1356 | "topLevel": false, 1357 | "builtIn": true, 1358 | "authenticationExecutions": [ 1359 | { 1360 | "authenticator": "idp-confirm-link", 1361 | "authenticatorFlow": false, 1362 | "requirement": "REQUIRED", 1363 | "priority": 10, 1364 | "autheticatorFlow": false, 1365 | "userSetupAllowed": false 1366 | }, 1367 | { 1368 | "authenticatorFlow": true, 1369 | "requirement": "REQUIRED", 1370 | "priority": 20, 1371 | "autheticatorFlow": true, 1372 | "flowAlias": "Account verification options", 1373 | "userSetupAllowed": false 1374 | } 1375 | ] 1376 | }, 1377 | { 1378 | "id": "a9083340-415c-40f1-b90f-6a92a084876a", 1379 | "alias": "Reset - Conditional OTP", 1380 | "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", 1381 | "providerId": "basic-flow", 1382 | "topLevel": false, 1383 | "builtIn": true, 1384 | "authenticationExecutions": [ 1385 | { 1386 | "authenticator": "conditional-user-configured", 1387 | "authenticatorFlow": false, 1388 | "requirement": "REQUIRED", 1389 | "priority": 10, 1390 | "autheticatorFlow": false, 1391 | "userSetupAllowed": false 1392 | }, 1393 | { 1394 | "authenticator": "reset-otp", 1395 | "authenticatorFlow": false, 1396 | "requirement": "REQUIRED", 1397 | "priority": 20, 1398 | "autheticatorFlow": false, 1399 | "userSetupAllowed": false 1400 | } 1401 | ] 1402 | }, 1403 | { 1404 | "id": "1e102233-630e-4a4e-aaed-ce79f72d6928", 1405 | "alias": "User creation or linking", 1406 | "description": "Flow for the existing/non-existing user alternatives", 1407 | "providerId": "basic-flow", 1408 | "topLevel": false, 1409 | "builtIn": true, 1410 | "authenticationExecutions": [ 1411 | { 1412 | "authenticatorConfig": "create unique user config", 1413 | "authenticator": "idp-create-user-if-unique", 1414 | "authenticatorFlow": false, 1415 | "requirement": "ALTERNATIVE", 1416 | "priority": 10, 1417 | "autheticatorFlow": false, 1418 | "userSetupAllowed": false 1419 | }, 1420 | { 1421 | "authenticatorFlow": true, 1422 | "requirement": "ALTERNATIVE", 1423 | "priority": 20, 1424 | "autheticatorFlow": true, 1425 | "flowAlias": "Handle Existing Account", 1426 | "userSetupAllowed": false 1427 | } 1428 | ] 1429 | }, 1430 | { 1431 | "id": "be40dba0-4ac8-434d-be3b-c204232060f9", 1432 | "alias": "Verify Existing Account by Re-authentication", 1433 | "description": "Reauthentication of existing account", 1434 | "providerId": "basic-flow", 1435 | "topLevel": false, 1436 | "builtIn": true, 1437 | "authenticationExecutions": [ 1438 | { 1439 | "authenticator": "idp-username-password-form", 1440 | "authenticatorFlow": false, 1441 | "requirement": "REQUIRED", 1442 | "priority": 10, 1443 | "autheticatorFlow": false, 1444 | "userSetupAllowed": false 1445 | }, 1446 | { 1447 | "authenticatorFlow": true, 1448 | "requirement": "CONDITIONAL", 1449 | "priority": 20, 1450 | "autheticatorFlow": true, 1451 | "flowAlias": "First broker login - Conditional OTP", 1452 | "userSetupAllowed": false 1453 | } 1454 | ] 1455 | }, 1456 | { 1457 | "id": "7118f5b1-fc16-47a1-8a3d-c705d47a9b00", 1458 | "alias": "browser", 1459 | "description": "browser based authentication", 1460 | "providerId": "basic-flow", 1461 | "topLevel": true, 1462 | "builtIn": true, 1463 | "authenticationExecutions": [ 1464 | { 1465 | "authenticator": "auth-cookie", 1466 | "authenticatorFlow": false, 1467 | "requirement": "ALTERNATIVE", 1468 | "priority": 10, 1469 | "autheticatorFlow": false, 1470 | "userSetupAllowed": false 1471 | }, 1472 | { 1473 | "authenticator": "auth-spnego", 1474 | "authenticatorFlow": false, 1475 | "requirement": "DISABLED", 1476 | "priority": 20, 1477 | "autheticatorFlow": false, 1478 | "userSetupAllowed": false 1479 | }, 1480 | { 1481 | "authenticator": "identity-provider-redirector", 1482 | "authenticatorFlow": false, 1483 | "requirement": "ALTERNATIVE", 1484 | "priority": 25, 1485 | "autheticatorFlow": false, 1486 | "userSetupAllowed": false 1487 | }, 1488 | { 1489 | "authenticatorFlow": true, 1490 | "requirement": "ALTERNATIVE", 1491 | "priority": 30, 1492 | "autheticatorFlow": true, 1493 | "flowAlias": "forms", 1494 | "userSetupAllowed": false 1495 | } 1496 | ] 1497 | }, 1498 | { 1499 | "id": "6ec610d9-be6b-43d7-9a64-720ebe9d4353", 1500 | "alias": "clients", 1501 | "description": "Base authentication for clients", 1502 | "providerId": "client-flow", 1503 | "topLevel": true, 1504 | "builtIn": true, 1505 | "authenticationExecutions": [ 1506 | { 1507 | "authenticator": "client-secret", 1508 | "authenticatorFlow": false, 1509 | "requirement": "ALTERNATIVE", 1510 | "priority": 10, 1511 | "autheticatorFlow": false, 1512 | "userSetupAllowed": false 1513 | }, 1514 | { 1515 | "authenticator": "client-jwt", 1516 | "authenticatorFlow": false, 1517 | "requirement": "ALTERNATIVE", 1518 | "priority": 20, 1519 | "autheticatorFlow": false, 1520 | "userSetupAllowed": false 1521 | }, 1522 | { 1523 | "authenticator": "client-secret-jwt", 1524 | "authenticatorFlow": false, 1525 | "requirement": "ALTERNATIVE", 1526 | "priority": 30, 1527 | "autheticatorFlow": false, 1528 | "userSetupAllowed": false 1529 | }, 1530 | { 1531 | "authenticator": "client-x509", 1532 | "authenticatorFlow": false, 1533 | "requirement": "ALTERNATIVE", 1534 | "priority": 40, 1535 | "autheticatorFlow": false, 1536 | "userSetupAllowed": false 1537 | } 1538 | ] 1539 | }, 1540 | { 1541 | "id": "0b608776-8576-494d-81b5-60f67b212143", 1542 | "alias": "direct grant", 1543 | "description": "OpenID Connect Resource Owner Grant", 1544 | "providerId": "basic-flow", 1545 | "topLevel": true, 1546 | "builtIn": true, 1547 | "authenticationExecutions": [ 1548 | { 1549 | "authenticator": "direct-grant-validate-username", 1550 | "authenticatorFlow": false, 1551 | "requirement": "REQUIRED", 1552 | "priority": 10, 1553 | "autheticatorFlow": false, 1554 | "userSetupAllowed": false 1555 | }, 1556 | { 1557 | "authenticator": "direct-grant-validate-password", 1558 | "authenticatorFlow": false, 1559 | "requirement": "REQUIRED", 1560 | "priority": 20, 1561 | "autheticatorFlow": false, 1562 | "userSetupAllowed": false 1563 | }, 1564 | { 1565 | "authenticatorFlow": true, 1566 | "requirement": "CONDITIONAL", 1567 | "priority": 30, 1568 | "autheticatorFlow": true, 1569 | "flowAlias": "Direct Grant - Conditional OTP", 1570 | "userSetupAllowed": false 1571 | } 1572 | ] 1573 | }, 1574 | { 1575 | "id": "89312bab-8131-43a9-892b-281478e58e9a", 1576 | "alias": "docker auth", 1577 | "description": "Used by Docker clients to authenticate against the IDP", 1578 | "providerId": "basic-flow", 1579 | "topLevel": true, 1580 | "builtIn": true, 1581 | "authenticationExecutions": [ 1582 | { 1583 | "authenticator": "docker-http-basic-authenticator", 1584 | "authenticatorFlow": false, 1585 | "requirement": "REQUIRED", 1586 | "priority": 10, 1587 | "autheticatorFlow": false, 1588 | "userSetupAllowed": false 1589 | } 1590 | ] 1591 | }, 1592 | { 1593 | "id": "2362cbe7-7658-4838-9c9a-e973b7e1d38d", 1594 | "alias": "first broker login", 1595 | "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", 1596 | "providerId": "basic-flow", 1597 | "topLevel": true, 1598 | "builtIn": true, 1599 | "authenticationExecutions": [ 1600 | { 1601 | "authenticatorConfig": "review profile config", 1602 | "authenticator": "idp-review-profile", 1603 | "authenticatorFlow": false, 1604 | "requirement": "REQUIRED", 1605 | "priority": 10, 1606 | "autheticatorFlow": false, 1607 | "userSetupAllowed": false 1608 | }, 1609 | { 1610 | "authenticatorFlow": true, 1611 | "requirement": "REQUIRED", 1612 | "priority": 20, 1613 | "autheticatorFlow": true, 1614 | "flowAlias": "User creation or linking", 1615 | "userSetupAllowed": false 1616 | } 1617 | ] 1618 | }, 1619 | { 1620 | "id": "5ee40a82-b91d-4c2f-a697-bbfddf6eaec4", 1621 | "alias": "forms", 1622 | "description": "Username, password, otp and other auth forms.", 1623 | "providerId": "basic-flow", 1624 | "topLevel": false, 1625 | "builtIn": true, 1626 | "authenticationExecutions": [ 1627 | { 1628 | "authenticator": "auth-username-password-form", 1629 | "authenticatorFlow": false, 1630 | "requirement": "REQUIRED", 1631 | "priority": 10, 1632 | "autheticatorFlow": false, 1633 | "userSetupAllowed": false 1634 | }, 1635 | { 1636 | "authenticatorFlow": true, 1637 | "requirement": "CONDITIONAL", 1638 | "priority": 20, 1639 | "autheticatorFlow": true, 1640 | "flowAlias": "Browser - Conditional OTP", 1641 | "userSetupAllowed": false 1642 | } 1643 | ] 1644 | }, 1645 | { 1646 | "id": "95238dfb-d40a-4ea7-b695-4bbcdfe5e40b", 1647 | "alias": "http challenge", 1648 | "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", 1649 | "providerId": "basic-flow", 1650 | "topLevel": true, 1651 | "builtIn": true, 1652 | "authenticationExecutions": [ 1653 | { 1654 | "authenticator": "no-cookie-redirect", 1655 | "authenticatorFlow": false, 1656 | "requirement": "REQUIRED", 1657 | "priority": 10, 1658 | "autheticatorFlow": false, 1659 | "userSetupAllowed": false 1660 | }, 1661 | { 1662 | "authenticatorFlow": true, 1663 | "requirement": "REQUIRED", 1664 | "priority": 20, 1665 | "autheticatorFlow": true, 1666 | "flowAlias": "Authentication Options", 1667 | "userSetupAllowed": false 1668 | } 1669 | ] 1670 | }, 1671 | { 1672 | "id": "25d02f2c-afc4-4e87-b177-f89b942dc81e", 1673 | "alias": "registration", 1674 | "description": "registration flow", 1675 | "providerId": "basic-flow", 1676 | "topLevel": true, 1677 | "builtIn": true, 1678 | "authenticationExecutions": [ 1679 | { 1680 | "authenticator": "registration-page-form", 1681 | "authenticatorFlow": true, 1682 | "requirement": "REQUIRED", 1683 | "priority": 10, 1684 | "autheticatorFlow": true, 1685 | "flowAlias": "registration form", 1686 | "userSetupAllowed": false 1687 | } 1688 | ] 1689 | }, 1690 | { 1691 | "id": "69183520-d2d0-4541-b819-672bf33dce33", 1692 | "alias": "registration form", 1693 | "description": "registration form", 1694 | "providerId": "form-flow", 1695 | "topLevel": false, 1696 | "builtIn": true, 1697 | "authenticationExecutions": [ 1698 | { 1699 | "authenticator": "registration-user-creation", 1700 | "authenticatorFlow": false, 1701 | "requirement": "REQUIRED", 1702 | "priority": 20, 1703 | "autheticatorFlow": false, 1704 | "userSetupAllowed": false 1705 | }, 1706 | { 1707 | "authenticator": "registration-profile-action", 1708 | "authenticatorFlow": false, 1709 | "requirement": "REQUIRED", 1710 | "priority": 40, 1711 | "autheticatorFlow": false, 1712 | "userSetupAllowed": false 1713 | }, 1714 | { 1715 | "authenticator": "registration-password-action", 1716 | "authenticatorFlow": false, 1717 | "requirement": "REQUIRED", 1718 | "priority": 50, 1719 | "autheticatorFlow": false, 1720 | "userSetupAllowed": false 1721 | }, 1722 | { 1723 | "authenticator": "registration-recaptcha-action", 1724 | "authenticatorFlow": false, 1725 | "requirement": "DISABLED", 1726 | "priority": 60, 1727 | "autheticatorFlow": false, 1728 | "userSetupAllowed": false 1729 | } 1730 | ] 1731 | }, 1732 | { 1733 | "id": "2f68d597-a359-4920-91e9-010f56254395", 1734 | "alias": "reset credentials", 1735 | "description": "Reset credentials for a user if they forgot their password or something", 1736 | "providerId": "basic-flow", 1737 | "topLevel": true, 1738 | "builtIn": true, 1739 | "authenticationExecutions": [ 1740 | { 1741 | "authenticator": "reset-credentials-choose-user", 1742 | "authenticatorFlow": false, 1743 | "requirement": "REQUIRED", 1744 | "priority": 10, 1745 | "autheticatorFlow": false, 1746 | "userSetupAllowed": false 1747 | }, 1748 | { 1749 | "authenticator": "reset-credential-email", 1750 | "authenticatorFlow": false, 1751 | "requirement": "REQUIRED", 1752 | "priority": 20, 1753 | "autheticatorFlow": false, 1754 | "userSetupAllowed": false 1755 | }, 1756 | { 1757 | "authenticator": "reset-password", 1758 | "authenticatorFlow": false, 1759 | "requirement": "REQUIRED", 1760 | "priority": 30, 1761 | "autheticatorFlow": false, 1762 | "userSetupAllowed": false 1763 | }, 1764 | { 1765 | "authenticatorFlow": true, 1766 | "requirement": "CONDITIONAL", 1767 | "priority": 40, 1768 | "autheticatorFlow": true, 1769 | "flowAlias": "Reset - Conditional OTP", 1770 | "userSetupAllowed": false 1771 | } 1772 | ] 1773 | }, 1774 | { 1775 | "id": "4a17f86b-8041-469f-b31e-6c5221eddfef", 1776 | "alias": "saml ecp", 1777 | "description": "SAML ECP Profile Authentication Flow", 1778 | "providerId": "basic-flow", 1779 | "topLevel": true, 1780 | "builtIn": true, 1781 | "authenticationExecutions": [ 1782 | { 1783 | "authenticator": "http-basic-authenticator", 1784 | "authenticatorFlow": false, 1785 | "requirement": "REQUIRED", 1786 | "priority": 10, 1787 | "autheticatorFlow": false, 1788 | "userSetupAllowed": false 1789 | } 1790 | ] 1791 | } 1792 | ], 1793 | "authenticatorConfig": [ 1794 | { 1795 | "id": "2467586c-ac0a-4eca-8ec8-2d1b277dbd14", 1796 | "alias": "create unique user config", 1797 | "config": { 1798 | "require.password.update.after.registration": "false" 1799 | } 1800 | }, 1801 | { 1802 | "id": "6590d87c-b615-495f-b1f3-bda56f86fb1c", 1803 | "alias": "review profile config", 1804 | "config": { 1805 | "update.profile.on.first.login": "missing" 1806 | } 1807 | } 1808 | ], 1809 | "requiredActions": [ 1810 | { 1811 | "alias": "CONFIGURE_TOTP", 1812 | "name": "Configure OTP", 1813 | "providerId": "CONFIGURE_TOTP", 1814 | "enabled": true, 1815 | "defaultAction": false, 1816 | "priority": 10, 1817 | "config": {} 1818 | }, 1819 | { 1820 | "alias": "terms_and_conditions", 1821 | "name": "Terms and Conditions", 1822 | "providerId": "terms_and_conditions", 1823 | "enabled": false, 1824 | "defaultAction": false, 1825 | "priority": 20, 1826 | "config": {} 1827 | }, 1828 | { 1829 | "alias": "UPDATE_PASSWORD", 1830 | "name": "Update Password", 1831 | "providerId": "UPDATE_PASSWORD", 1832 | "enabled": true, 1833 | "defaultAction": false, 1834 | "priority": 30, 1835 | "config": {} 1836 | }, 1837 | { 1838 | "alias": "UPDATE_PROFILE", 1839 | "name": "Update Profile", 1840 | "providerId": "UPDATE_PROFILE", 1841 | "enabled": true, 1842 | "defaultAction": false, 1843 | "priority": 40, 1844 | "config": {} 1845 | }, 1846 | { 1847 | "alias": "VERIFY_EMAIL", 1848 | "name": "Verify Email", 1849 | "providerId": "VERIFY_EMAIL", 1850 | "enabled": true, 1851 | "defaultAction": false, 1852 | "priority": 50, 1853 | "config": {} 1854 | }, 1855 | { 1856 | "alias": "delete_account", 1857 | "name": "Delete Account", 1858 | "providerId": "delete_account", 1859 | "enabled": false, 1860 | "defaultAction": false, 1861 | "priority": 60, 1862 | "config": {} 1863 | }, 1864 | { 1865 | "alias": "update_user_locale", 1866 | "name": "Update User Locale", 1867 | "providerId": "update_user_locale", 1868 | "enabled": true, 1869 | "defaultAction": false, 1870 | "priority": 1000, 1871 | "config": {} 1872 | } 1873 | ], 1874 | "browserFlow": "browser", 1875 | "registrationFlow": "registration", 1876 | "directGrantFlow": "direct grant", 1877 | "resetCredentialsFlow": "reset credentials", 1878 | "clientAuthenticationFlow": "clients", 1879 | "dockerAuthenticationFlow": "docker auth", 1880 | "attributes": { 1881 | "cibaBackchannelTokenDeliveryMode": "poll", 1882 | "cibaExpiresIn": "120", 1883 | "cibaAuthRequestedUserHint": "login_hint", 1884 | "oauth2DeviceCodeLifespan": "600", 1885 | "oauth2DevicePollingInterval": "5", 1886 | "parRequestUriLifespan": "60", 1887 | "cibaInterval": "5" 1888 | }, 1889 | "keycloakVersion": "18.0.0", 1890 | "userManagedAccessAllowed": false, 1891 | "clientProfiles": { 1892 | "profiles": [] 1893 | }, 1894 | "clientPolicies": { 1895 | "policies": [] 1896 | } 1897 | } --------------------------------------------------------------------------------