├── customer ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties └── src │ └── main │ ├── resources │ ├── banner.txt │ ├── db │ │ ├── schema.sql │ │ └── data.sql │ └── application.yml │ └── java │ └── dev │ └── nano │ └── customer │ ├── CustomerRepository.java │ ├── CustomerMapper.java │ ├── CustomerConstant.java │ ├── CustomerService.java │ ├── CustomerApplication.java │ ├── CustomerDTO.java │ ├── CustomerEntity.java │ └── config │ └── SecurityConfig.java ├── docs └── diagrams │ ├── architecture-diagram.png │ ├── infrastructure-diagram.png │ └── deploy-workflow-diagram.png ├── payment └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── payment │ │ ├── PaymentStatus.java │ │ ├── PaymentService.java │ │ ├── PaymentMapper.java │ │ ├── PaymentRepository.java │ │ ├── PaymentConstant.java │ │ ├── PaymentDTO.java │ │ ├── PaymentApplication.java │ │ ├── PaymentEntity.java │ │ ├── config │ │ └── SecurityConfig.java │ │ └── PaymentProcessor.java │ └── resources │ ├── banner.txt │ ├── application.yml │ └── db │ ├── schema.sql │ └── data.sql ├── apiKey-manager └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ ├── apikey │ │ ├── KeyGenerator.java │ │ ├── ApiKeyConstant.java │ │ ├── ApiKeyRequest.java │ │ ├── UUIDKeyGeneratorImpl.java │ │ ├── ApiKeyService.java │ │ ├── ApiKeyRepository.java │ │ └── ApiKeyEntity.java │ │ ├── application │ │ ├── ApplicationName.java │ │ ├── ApplicationService.java │ │ ├── ApplicationConstant.java │ │ ├── ApplicationRepository.java │ │ ├── ApplicationEntity.java │ │ └── ApplicationServiceImpl.java │ │ ├── ApiKeyManagerApplication.java │ │ └── controller │ │ ├── ApplicationController.java │ │ └── ApiKeyController.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ ├── schema.sql │ └── data.sql ├── notification └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── notification │ │ ├── email │ │ ├── EmailService.java │ │ └── DefaultEmailService.java │ │ ├── NotificationRepository.java │ │ ├── aws │ │ ├── AWSConstant.java │ │ ├── AWSEmailService.java │ │ └── AWSConfig.java │ │ ├── NotificationService.java │ │ ├── NotificationMapper.java │ │ ├── NotificationConstant.java │ │ ├── NotificationApplication.java │ │ ├── rabbitmq │ │ └── NotificationConsumer.java │ │ ├── NotificationDTO.java │ │ ├── config │ │ └── SecurityConfig.java │ │ ├── NotificationEntity.java │ │ └── NotificationServiceImpl.java │ └── resources │ ├── banner.txt │ ├── db │ ├── schema.sql │ └── data.sql │ └── application.yml ├── amqp ├── src │ └── main │ │ ├── resources │ │ ├── amqp-default.properties │ │ ├── amqp-docker.properties │ │ ├── amqp-eks.properties │ │ └── amqp-kube.properties │ │ └── java │ │ └── dev │ │ └── nano │ │ └── amqp │ │ ├── RabbitMQProducer.java │ │ └── RabbitMQConfig.java └── pom.xml ├── k8s ├── minikube │ ├── bootstrap │ │ ├── postgres │ │ │ ├── secret.yml │ │ │ ├── configmap.yml │ │ │ ├── service.yml │ │ │ ├── volume.yml │ │ │ └── statefulset.yml │ │ ├── grafana │ │ │ ├── service.yml │ │ │ ├── configmap.yml │ │ │ └── deployment.yml │ │ ├── keycloak │ │ │ ├── service.yml │ │ │ └── deployment.yml │ │ ├── zipkin │ │ │ ├── service.yml │ │ │ └── statefulset.yml │ │ ├── prometheus │ │ │ ├── service.yml │ │ │ ├── cluster-role.yml │ │ │ ├── deployment.yml │ │ │ └── configmap.yml │ │ ├── rabbitmq │ │ │ ├── service.yml │ │ │ ├── rbac.yml │ │ │ ├── configmap.yml │ │ │ └── statefulset.yml │ │ └── ingress │ │ │ ├── readme.MD │ │ │ └── ingress.yml │ └── services │ │ ├── order │ │ ├── service.yml │ │ └── deployment.yml │ │ ├── payment │ │ ├── service.yml │ │ └── deployment.yml │ │ ├── product │ │ ├── service.yml │ │ └── deployment.yml │ │ ├── apikey-manager │ │ ├── service.yml │ │ └── deployment.yml │ │ ├── customer │ │ ├── service.yml │ │ └── deployment.yml │ │ └── notification │ │ ├── service.yml │ │ └── deployment.yml └── aws-eks │ ├── services │ ├── order │ │ ├── service.yml │ │ ├── servicemonitor.yml │ │ └── deployment.yml │ ├── payment │ │ ├── service.yml │ │ ├── servicemonitor.yml │ │ └── deployment.yml │ ├── product │ │ ├── service.yml │ │ ├── servicemonitor.yml │ │ └── deployment.yml │ ├── customer │ │ ├── service.yml │ │ ├── servicemonitor.yml │ │ └── deployment.yml │ └── notification │ │ ├── service.yml │ │ ├── servicemonitor.yml │ │ └── deployment.yml │ └── bootstrap │ ├── zipkin │ ├── service.yml │ └── statefulset.yml │ ├── prometheus │ ├── service.yml │ └── prometheus.yml │ └── rabbitmq │ ├── service.yml │ ├── rbac.yml │ ├── configmap.yml │ └── statefulset.yml ├── order └── src │ └── main │ ├── resources │ ├── banner.txt │ ├── application.yml │ └── db │ │ ├── data.sql │ │ └── schema.sql │ └── java │ └── dev │ └── nano │ └── order │ ├── OrderRepository.java │ ├── OrderService.java │ ├── OrderMapper.java │ ├── OrderConstant.java │ ├── OrderDTO.java │ ├── OrderApplication.java │ ├── OrderEntity.java │ ├── config │ ├── SecurityConfig.java │ └── ApiKeyAuthFilter.java │ └── OrderServiceImpl.java ├── gateway └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── gateway │ │ ├── security │ │ ├── ApiKeyAuthorizationChecker.java │ │ ├── KeycloakRoleConverter.java │ │ ├── SecurityConfig.java │ │ ├── ApiAuthorizationFilter.java │ │ └── ApiKeyManagerAuthorizationChecker.java │ │ ├── GatewayConfig.java │ │ └── GatewayApplication.java │ └── resources │ ├── banner.txt │ └── application.yml ├── docker ├── config │ ├── postgres │ │ └── init.sql │ │ │ └── init.sql │ └── prometheus │ │ └── prometheus.yml └── compose │ └── docker-compose-local.yml ├── common ├── src │ └── main │ │ ├── java │ │ └── dev │ │ │ └── nano │ │ │ ├── exceptionhandler │ │ │ ├── core │ │ │ │ ├── BaseException.java │ │ │ │ ├── BadRequestException.java │ │ │ │ ├── ResourceNotFoundException.java │ │ │ │ ├── DuplicateResourceException.java │ │ │ │ └── ValidationException.java │ │ │ ├── payload │ │ │ │ ├── ValidationError.java │ │ │ │ ├── ErrorDetails.java │ │ │ │ └── ErrorCode.java │ │ │ └── business │ │ │ │ ├── OrderException.java │ │ │ │ ├── ProductException.java │ │ │ │ ├── CustomerException.java │ │ │ │ ├── PaymentException.java │ │ │ │ └── NotificationException.java │ │ │ ├── cache │ │ │ └── CacheConfig.java │ │ │ ├── feign │ │ │ └── FeignClientInterceptor.java │ │ │ ├── swagger │ │ │ ├── OpenAPIProperties.java │ │ │ ├── BaseController.java │ │ │ └── OpenAPIConfig.java │ │ │ └── security │ │ │ └── JwtAuthConverter.java │ │ └── resources │ │ ├── application-openapi.yml │ │ ├── shared-application-kube.yml │ │ ├── shared-application-eks.yml │ │ ├── shared-application-docker.yml │ │ └── shared-application-default.yml └── pom.xml ├── feign-clients ├── src │ └── main │ │ ├── java │ │ └── dev │ │ │ └── nano │ │ │ └── clients │ │ │ ├── customer │ │ │ ├── CustomerResponse.java │ │ │ └── CustomerClient.java │ │ │ ├── product │ │ │ ├── ProductResponse.java │ │ │ └── ProductClient.java │ │ │ ├── payment │ │ │ ├── PaymentRequest.java │ │ │ ├── PaymentResponse.java │ │ │ └── PaymentClient.java │ │ │ ├── order │ │ │ ├── OrderRequest.java │ │ │ ├── OrderResponse.java │ │ │ └── OrderClient.java │ │ │ ├── notification │ │ │ ├── NotificationResponse.java │ │ │ ├── NotificationRequest.java │ │ │ └── NotificationClient.java │ │ │ ├── config │ │ │ └── InternalApiKeyFeignConfig.java │ │ │ └── apiKeyManager │ │ │ └── apiKey │ │ │ └── ApiKeyManagerClient.java │ │ └── resources │ │ ├── clients-eks.properties │ │ ├── clients-docker.properties │ │ ├── clients-default.properties │ │ └── clients-kube.properties └── pom.xml ├── product └── src │ └── main │ ├── resources │ ├── application.yml │ ├── banner.txt │ └── db │ │ ├── schema.sql │ │ └── data.sql │ └── java │ └── dev │ └── nano │ └── product │ ├── ProductService.java │ ├── ProductMapper.java │ ├── ProductConstant.java │ ├── ProductApplication.java │ ├── ProductRepository.java │ ├── ProductDTO.java │ ├── ProductEntity.java │ ├── config │ └── SecurityConfig.java │ └── ProductServiceImpl.java ├── eureka-server ├── src │ └── main │ │ ├── resources │ │ ├── banner.txt │ │ ├── application-docker.yml │ │ └── application.yml │ │ └── java │ │ └── dev │ │ └── nano │ │ └── eurekaserver │ │ ├── EurekaServerApplication.java │ │ └── config │ │ └── SecurityConfig.java └── pom.xml ├── .github └── workflows │ └── build.yml ├── .gitignore └── skaffold.yaml /customer/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/HEAD/customer/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /docs/diagrams/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/HEAD/docs/diagrams/architecture-diagram.png -------------------------------------------------------------------------------- /docs/diagrams/infrastructure-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/HEAD/docs/diagrams/infrastructure-diagram.png -------------------------------------------------------------------------------- /docs/diagrams/deploy-workflow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/HEAD/docs/diagrams/deploy-workflow-diagram.png -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentStatus.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | public enum PaymentStatus { 4 | PENDING, 5 | COMPLETED 6 | } -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/KeyGenerator.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | public interface KeyGenerator { 4 | String generateKey(); 5 | } 6 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/email/EmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.email; 2 | 3 | public interface EmailService { 4 | void send(String to, String content, String subject); 5 | } 6 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-default.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-docker.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-eks.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-kube.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secret 5 | type: Opaque 6 | data: 7 | DB_USERNAME: cG9zdGdyZXM= # postgres 8 | DB_PASSWORD: cGFzc3dvcmQ= # password -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: postgres-config 5 | data: 6 | POSTGRES_DB: miliariadnane 7 | POSTGRES_USER: miliariadnane 8 | POSTGRES_PASSWORD: password 9 | -------------------------------------------------------------------------------- /order/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,-----. ,--. 2 | ' .-. ' ,--.--. ,-| | ,---. ,--.--. 3 | | | | | | .--' ' .-. | | .-. : | .--' 4 | ' '-' ' | | \ `-' | \ --. | | 5 | `-----' `--' `---' `----' `--' 6 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiKeyAuthorizationChecker.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | public interface ApiKeyAuthorizationChecker { 4 | boolean isAuthorized(String apiKey, String applicationName); 5 | } 6 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: order 5 | spec: 6 | selector: 7 | app: order 8 | ports: 9 | - port: 80 10 | targetPort: 8003 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/order/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: order 5 | spec: 6 | selector: 7 | app: order 8 | ports: 9 | - port: 80 10 | targetPort: 8003 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payment 5 | spec: 6 | selector: 7 | app: payment 8 | ports: 9 | - port: 80 10 | targetPort: 8005 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: product 5 | spec: 6 | selector: 7 | app: product 8 | ports: 9 | - port: 80 10 | targetPort: 8002 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/payment/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payment 5 | spec: 6 | selector: 7 | app: payment 8 | ports: 9 | - port: 80 10 | targetPort: 8005 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/product/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: product 5 | spec: 6 | selector: 7 | app: product 8 | ports: 9 | - port: 80 10 | targetPort: 8002 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: customer 5 | spec: 6 | selector: 7 | app: customer 8 | ports: 9 | - port: 80 10 | targetPort: 8001 11 | type: LoadBalancer 12 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres 5 | spec: 6 | selector: 7 | app: postgres 8 | ports: 9 | - port: 5432 10 | targetPort: 5432 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /docker/config/postgres/init.sql/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE notification; 2 | CREATE DATABASE customer; 3 | CREATE DATABASE product; 4 | CREATE DATABASE "order"; 5 | CREATE DATABASE payment; 6 | CREATE DATABASE "apikey-manager"; 7 | CREATE DATABASE keycloak; 8 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: notification 5 | spec: 6 | selector: 7 | app: notification 8 | ports: 9 | - port: 80 10 | targetPort: 8004 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationName.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public enum ApplicationName { 4 | CUSTOMER, 5 | PRODUCT, 6 | ORDER, 7 | PAYMENT, 8 | NOTIFICATION, 9 | APIKEY_MANAGER 10 | } 11 | -------------------------------------------------------------------------------- /k8s/minikube/services/apikey-manager/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: apikey-manager 5 | spec: 6 | selector: 7 | app: apikey-manager 8 | ports: 9 | - protocol: TCP 10 | port: 8006 11 | targetPort: 8006 -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/core/BaseException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.core; 2 | 3 | public abstract class BaseException extends RuntimeException { 4 | public BaseException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/grafana/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: grafana 5 | spec: 6 | selector: 7 | app: grafana 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 3000 12 | targetPort: 3000 -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/core/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.core; 2 | 3 | public class BadRequestException extends BaseException { 4 | public BadRequestException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/zipkin/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zipkin 5 | spec: 6 | selector: 7 | app: zipkin 8 | ports: 9 | - port: 9411 10 | targetPort: 9411 11 | protocol: TCP 12 | type: LoadBalancer 13 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/keycloak/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: keycloak 5 | spec: 6 | selector: 7 | app: keycloak 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 8080 12 | targetPort: 8080 -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/zipkin/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zipkin 5 | spec: 6 | selector: 7 | app: zipkin 8 | ports: 9 | - port: 9411 10 | targetPort: 9411 11 | protocol: TCP 12 | type: LoadBalancer 13 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8006 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: apikey-manager 9 | config: 10 | import: classpath:shared-application-${spring.profiles.active}.yml 11 | -------------------------------------------------------------------------------- /customer/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/prometheus/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus 5 | spec: 6 | selector: 7 | app: prometheus 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 9090 12 | targetPort: 9090 -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/customer/CustomerResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.customer; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class CustomerResponse { 7 | private Long id; 8 | private String name; 9 | private String email; 10 | } -------------------------------------------------------------------------------- /product/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8002 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: product 9 | config: 10 | import: 11 | - "classpath:shared-application-${spring.profiles.active}.yml" 12 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/product/ProductResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.product; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ProductResponse { 7 | private String name; 8 | private String image; 9 | private Integer price; 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/core/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.core; 2 | 3 | public class ResourceNotFoundException extends BaseException { 4 | public ResourceNotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | public class ApiKeyConstant { 4 | public static final String API_KEY_URI_REST_API = "/api/v1/apiKey-manager/api-keys"; 5 | public static final String API_KEY_NOT_FOUND = "Api key not found"; 6 | } 7 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/core/DuplicateResourceException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.core; 2 | 3 | public class DuplicateResourceException extends BaseException { 4 | public DuplicateResourceException(String message) { 5 | super(message); 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface OrderRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public interface ApplicationService { 4 | void assignApplicationToApiKey(ApplicationName applicationName, String apiKey); 5 | void revokeApplicationFromApiKey(String applicationName, String apiKey); 6 | } 7 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/payload/ValidationError.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.payload; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @Builder 8 | public class ValidationError { 9 | private String field; 10 | private String message; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | 5 | import java.util.List; 6 | 7 | public record ApiKeyRequest( 8 | String client, 9 | String description, 10 | List applications 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public class ApplicationConstant { 4 | public static final String APPLICATION_URI_REST_API = "/api/v1/apiKey-manager/applications"; 5 | public static final String APPLICATION_NOT_FOUND = "Application not found"; 6 | } 7 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class PaymentRequest { 7 | private Long customerId; 8 | private String customerName; 9 | private String customerEmail; 10 | private Long orderId; 11 | } 12 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import dev.nano.clients.order.OrderRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface OrderService { 8 | OrderDTO getOrder(Long id); 9 | List getAllOrders(); 10 | OrderDTO createOrder(OrderRequest order); 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/business/OrderException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.business; 2 | 3 | import dev.nano.exceptionhandler.core.BaseException; 4 | 5 | public class OrderException extends BaseException { 6 | public OrderException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/business/ProductException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.business; 2 | 3 | import dev.nano.exceptionhandler.core.BaseException; 4 | 5 | public class ProductException extends BaseException { 6 | public ProductException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface NotificationRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/business/CustomerException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.business; 2 | 3 | import dev.nano.exceptionhandler.core.BaseException; 4 | 5 | public class CustomerException extends BaseException { 6 | public CustomerException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/business/PaymentException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.business; 2 | 3 | import dev.nano.exceptionhandler.core.BaseException; 4 | 5 | public class PaymentException extends BaseException { 6 | public PaymentException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /customer/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,-----. ,--. 2 | ' .--./ ,--.,--. ,---. ,-' '-. ,---. ,--,--,--. ,---. ,--.--. 3 | | | | || | ( .-' '-. .-' | .-. | | | | .-. : | .--' 4 | ' '--'\ ' '' ' .-' `) | | ' '-' ' | | | | \ --. | | 5 | `-----' `----' `----' `--' `---' `--`--`--' `----' `--' 6 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/business/NotificationException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.business; 2 | 3 | import dev.nano.exceptionhandler.core.BaseException; 4 | 5 | public class NotificationException extends BaseException { 6 | public NotificationException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OrderRequest { 7 | private Long customerId; 8 | private String customerName; 9 | private String customerEmail; 10 | private Long productId; 11 | private Integer amount; 12 | } 13 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: customer 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/customer/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: customer -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: orders 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/order/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: orders 15 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: payment 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/payment/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: payment -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: product 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/product/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: product -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | public class AWSConstant { 4 | 5 | public static final String UTF_ENCODING = "UTF-8"; 6 | public static final String SEND_EMAIL_FAILED_EXCEPTION = "Failed to send email"; 7 | public static final String SENDER = "miliari.adnane@gmail.com"; 8 | } 9 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class PaymentResponse { 9 | private Long id; 10 | private Long customerId; 11 | private Long orderId; 12 | private LocalDateTime createAt; 13 | } 14 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface CustomerRepository extends JpaRepository { 8 | boolean existsByEmail(String email); 9 | } 10 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/grafana/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: grafana-datasources 5 | data: 6 | prometheus.yaml: | 7 | apiVersion: 1 8 | datasources: 9 | - name: Prometheus 10 | type: prometheus 11 | url: http://prometheus:9090 12 | isDefault: true 13 | access: proxy 14 | editable: true -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import dev.nano.clients.payment.PaymentRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface PaymentService { 8 | PaymentDTO getPayment(Long paymentId); 9 | List getAllPayments(); 10 | PaymentDTO createPayment(PaymentRequest payment); 11 | } 12 | -------------------------------------------------------------------------------- /payment/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. 2 | | .--. ' ,--,--. ,--. ,--. ,--,--,--. ,---. ,--,--, ,-' '-. 3 | | '--' | ' ,-. | \ ' / | | | .-. : | \ '-. .-' 4 | | | --' \ '-' | \ ' | | | | \ --. | || | | | 5 | `--' `--`--' .-' / `--`--`--' `----' `--''--' `--' -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-eks.properties: -------------------------------------------------------------------------------- 1 | # we don't need to specify the port in k8s because it's listening on port 80 2 | clients.customer.url=http://customer 3 | clients.product.url=http://product 4 | clients.order.url=http://order 5 | clients.notification.url=http://notification 6 | clients.payment.url=http://payment 7 | clients.apiKey-manager.url=http://apiKey-manager 8 | -------------------------------------------------------------------------------- /customer/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS customer_sequence START WITH 1 INCREMENT BY 1; 3 | 4 | CREATE TABLE IF NOT EXISTS customer 5 | ( 6 | id BIGINT DEFAULT nextval('customer_sequence') PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | email TEXT NOT NULL UNIQUE, 9 | phone TEXT, 10 | address TEXT 11 | ); 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: notification 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/notification/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: notification -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface OrderMapper { 9 | OrderEntity toEntity(OrderDTO dto); 10 | OrderDTO toDTO(OrderEntity entity); 11 | List toListDTO(List listEntity); 12 | } 13 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/UUIDKeyGeneratorImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.UUID; 6 | 7 | @Component 8 | public class UUIDKeyGeneratorImpl implements KeyGenerator { 9 | @Override 10 | public String generateKey() { 11 | return UUID.randomUUID().toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class OrderResponse { 9 | private Long id; 10 | private Long customerId; 11 | private Long productId; 12 | private Integer amount; 13 | private LocalDateTime createAt; 14 | } 15 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/prometheus/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus-service 5 | namespace: monitoring 6 | spec: 7 | clusterIP: None 8 | ports: 9 | - name: web 10 | port: 9090 11 | protocol: TCP 12 | targetPort: 9090 13 | selector: 14 | prometheus: applications 15 | sessionAffinity: None 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /gateway/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,----. ,--. 2 | ' .-./ ,--,--. ,-' '-. ,---. ,--. ,--. ,--,--. ,--. ,--. 3 | | | .---. ' ,-. | '-. .-' | .-. : | |.'.| | ' ,-. | \ ' / 4 | ' '--' | \ '-' | | | \ --. | .'. | \ '-' | \ ' 5 | `------' `--`--' `--' `----' '--' '--' `--`--' .-' / 6 | `---' -------------------------------------------------------------------------------- /k8s/minikube/services/customer/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: customer 5 | spec: 6 | selector: 7 | app: customer # has to match .metadata.labels.app && template.metadata.labels.app of deployment || stafulset 8 | ports: 9 | - port: 80 10 | targetPort: 8001 # has to match containerPort of deployment || stafulset 11 | type: LoadBalancer 12 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-docker.properties: -------------------------------------------------------------------------------- 1 | clients.customer.url=http://customer:8001 2 | clients.product.url=http://product:8002 3 | clients.order.url=http://order:8003 4 | clients.notification.url=http://notification:8004 5 | clients.payment.url=http://payment:8005 6 | clients.apiKey-manager.url=http://apikey-manager:8006/api/v1/apiKey-manager/api-keys 7 | internal.api-key=internal-service-key 8 | -------------------------------------------------------------------------------- /k8s/minikube/services/notification/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: notification 5 | spec: 6 | selector: 7 | app: notification # has to match .metadata.labels.app && template.metadata.labels.app of deployment || stafulset 8 | ports: 9 | - port: 80 10 | targetPort: 8004 # has to match containerPort of deployment || stafulset 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /product/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. ,--. 2 | | .--. ' ,--.--. ,---. ,-| | ,--.,--. ,---. ,-' '-. 3 | | '--' | | .--' | .-. | ' .-. | | || | | .--' '-. .-' 4 | | | --' | | ' '-' ' \ `-' | ' '' ' \ `--. | | 5 | `--' `--' `---' `---' `----' `---' `--' 6 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface ApplicationRepository extends JpaRepository { 8 | Optional findByApplicationName(String applicationName); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-default.properties: -------------------------------------------------------------------------------- 1 | clients.customer.url=http://localhost:8001 2 | clients.product.url=http://localhost:8002 3 | clients.order.url=http://localhost:8003 4 | clients.notification.url=http://localhost:8004 5 | clients.payment.url=http://localhost:8005 6 | clients.apiKey-manager.url=http://localhost:8006/api/v1/apiKey-manager/api-keys 7 | internal.api-key=internal-service-key 8 | -------------------------------------------------------------------------------- /order/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8003 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: order 9 | config: 10 | import: 11 | - "classpath:shared-application-${spring.profiles.active}.yml" 12 | - "classpath:clients-${spring.profiles.active}.properties" 13 | - "classpath:amqp-${spring.profiles.active}.properties" 14 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.mapstruct.Mapper; 4 | import java.util.List; 5 | 6 | @Mapper(componentModel = "spring") 7 | public interface PaymentMapper { 8 | PaymentDTO toDTO(PaymentEntity entity); 9 | PaymentEntity toEntity(PaymentDTO dto); 10 | List toListDTO(List entities); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /payment/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8005 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: payment 9 | config: 10 | import: 11 | - "classpath:shared-application-${spring.profiles.active}.yml" 12 | - "classpath:clients-${spring.profiles.active}.properties" 13 | - "classpath:amqp-${spring.profiles.active}.properties" 14 | -------------------------------------------------------------------------------- /customer/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8001 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: customer 9 | config: 10 | import: 11 | - "classpath:shared-application-${spring.profiles.active}.yml" 12 | - "classpath:clients-${spring.profiles.active}.properties" 13 | - "classpath:amqp-${spring.profiles.active}.properties" 14 | 15 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.List; 7 | 8 | @Repository 9 | public interface PaymentRepository extends JpaRepository { 10 | List findAllByStatus(PaymentStatus status); 11 | } 12 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import java.util.List; 4 | 5 | public interface ProductService { 6 | ProductDTO create(ProductDTO product); 7 | ProductDTO getProduct(long productId); 8 | List getAllProducts(int page, int limit, String search); 9 | ProductDTO update(long id, ProductDTO request); 10 | void delete(long id); 11 | } 12 | -------------------------------------------------------------------------------- /order/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO "order" (id, customer_id, product_id, amount, create_at) 2 | VALUES (nextval('order_sequence'), 1, 1, 2, CURRENT_TIMESTAMP), 3 | (nextval('order_sequence'), 1, 3, 1, CURRENT_TIMESTAMP), 4 | (nextval('order_sequence'), 2, 2, 3, CURRENT_TIMESTAMP), 5 | (nextval('order_sequence'), 3, 4, 1, CURRENT_TIMESTAMP), 6 | (nextval('order_sequence'), 4, 1, 2, CURRENT_TIMESTAMP); 7 | -------------------------------------------------------------------------------- /payment/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS payment_sequence START WITH 1 INCREMENT BY 1; 3 | 4 | CREATE TABLE IF NOT EXISTS payment 5 | ( 6 | id BIGINT DEFAULT nextval('payment_sequence') PRIMARY KEY, 7 | customer_id BIGINT NOT NULL, 8 | order_id BIGINT NOT NULL, 9 | create_at TIMESTAMP NOT NULL, 10 | status VARCHAR(50) NOT NULL 11 | ); 12 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface NotificationService { 8 | NotificationDTO getNotification(Long notificationId); 9 | List getAllNotification(); 10 | void sendNotification(NotificationRequest notificationRequest); 11 | } 12 | -------------------------------------------------------------------------------- /order/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS order_sequence START WITH 1 INCREMENT BY 1; 3 | 4 | CREATE TABLE IF NOT EXISTS "order" 5 | ( 6 | id BIGINT DEFAULT nextval('order_sequence') PRIMARY KEY, 7 | customer_id BIGINT NOT NULL, 8 | product_id BIGINT NOT NULL, 9 | amount DOUBLE PRECISION NOT NULL, 10 | create_at TIMESTAMP NOT NULL 11 | ); 12 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface NotificationMapper { 9 | NotificationEntity toEntity(NotificationDTO dto); 10 | NotificationDTO toDTO(NotificationEntity entity); 11 | List toListDTO(List listEntity); 12 | } 13 | -------------------------------------------------------------------------------- /product/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS product_sequence START WITH 1 INCREMENT BY 1; 3 | 4 | CREATE TABLE IF NOT EXISTS product 5 | ( 6 | id BIGINT DEFAULT nextval('product_sequence') PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | image TEXT, 9 | price INTEGER NOT NULL, 10 | available_quantity INTEGER NOT NULL DEFAULT 0 11 | ); 12 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | public interface ApiKeyService { 7 | String save(ApiKeyRequest apiKeyRequest); 8 | void revokeApi(String key); 9 | @Transactional(readOnly = true) 10 | boolean isAuthorized(String apiKey, ApplicationName applicationName); 11 | } 12 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class NotificationResponse { 9 | private Long customerId; 10 | private String customerName; 11 | private String customerEmail; 12 | private String sender; 13 | private String message; 14 | private LocalDateTime sentAt; 15 | } 16 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/GatewayConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.codec.ServerCodecConfigurer; 6 | 7 | @Configuration 8 | public class GatewayConfig { 9 | 10 | @Bean 11 | public ServerCodecConfigurer serverCodecConfigurer() { 12 | return ServerCodecConfigurer.create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notification/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,--. ,--. ,--. ,--. ,---. ,--. ,--. ,--. 2 | | ,'.| | ,---. ,-' '-. `--' / .-' `--' ,---. ,--,--. ,-' '-. `--' ,---. ,--,--, 3 | | |' ' | | .-. | '-. .-' ,--. | `-, ,--. | .--' ' ,-. | '-. .-' ,--. | .-. | | \ 4 | | | ` | ' '-' ' | | | | | .-' | | \ `--. \ '-' | | | | | ' '-' ' | || | 5 | `--' `--' `---' `--' `--' `--' `--' `---' `--`--' `--' `--' `---' `--''--' 6 | -------------------------------------------------------------------------------- /payment/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO payment (id, customer_id, order_id, create_at, status) 2 | VALUES (nextval('payment_sequence'), 1, 1, CURRENT_TIMESTAMP, 'COMPLETED'), 3 | (nextval('payment_sequence'), 1, 2, CURRENT_TIMESTAMP, 'COMPLETED'), 4 | (nextval('payment_sequence'), 2, 3, CURRENT_TIMESTAMP, 'COMPLETED'), 5 | (nextval('payment_sequence'), 3, 4, CURRENT_TIMESTAMP, 'PENDING'), 6 | (nextval('payment_sequence'), 4, 5, CURRENT_TIMESTAMP, 'PENDING'); 7 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: rabbitmq 5 | labels: 6 | app: rabbitmq 7 | type: LoadBalancer 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 15672 14 | targetPort: 15672 15 | nodePort: 31672 16 | - name: amqp 17 | protocol: TCP 18 | port: 5672 19 | targetPort: 5672 20 | nodePort: 30672 21 | selector: 22 | app: rabbitmq 23 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: rabbitmq 5 | labels: 6 | app: rabbitmq 7 | type: LoadBalancer 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 15672 14 | targetPort: 15672 15 | nodePort: 31672 16 | - name: amqp 17 | protocol: TCP 18 | port: 5672 19 | targetPort: 5672 20 | nodePort: 30672 21 | selector: 22 | app: rabbitmq 23 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/payload/ErrorDetails.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.payload; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | @Getter 10 | @Builder 11 | public class ErrorDetails { 12 | private LocalDateTime timestamp; 13 | private String code; 14 | private String message; 15 | private String path; 16 | private List errors; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class OrderConstant { 7 | public static final String ORDER_URI_REST_API = "/api/v1/orders"; 8 | public static final String ORDER_NOT_FOUND = "Order with ID %d not found"; 9 | public static final String ORDER_CREATE_ERROR = "Failed to create order: %s"; 10 | public static final String NO_ORDERS_FOUND = "No orders found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. ,---. 2 | | .---' ,--.,--. ,--.--. ,---. | |,-. ,--,--. ' .-' ,---. ,--.--. ,--. ,--. ,---. ,--.--. 3 | | `--, | || | | .--' | .-. : | / ' ,-. | `. `-. | .-. : | .--' \ `' / | .-. : | .--' 4 | | `---. ' '' ' | | \ --. | \ \ \ '-' | .-' | \ --. | | \ / \ --. | | 5 | `------' `----' `--' `----' `--'`--' `--`--' `-----' `----' `--' `--' `----' `--' -------------------------------------------------------------------------------- /notification/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS notification_sequence START WITH 1 INCREMENT BY 1; 3 | 4 | CREATE TABLE IF NOT EXISTS notification 5 | ( 6 | id BIGINT DEFAULT nextval('notification_sequence') PRIMARY KEY, 7 | customer_id BIGINT NOT NULL, 8 | customer_name TEXT, 9 | customer_email TEXT, 10 | sender TEXT NOT NULL, 11 | message TEXT NOT NULL, 12 | sent_at TIMESTAMP NOT NULL 13 | ); 14 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-kube.properties: -------------------------------------------------------------------------------- 1 | # we don't need to specify the port in k8s because it listening on port 80 2 | clients.customer.url=http://localhost/api/v1/customer 3 | clients.product.url=http://localhost/api/v1/product 4 | clients.order.url=http://localhost/api/v1/order 5 | clients.notification.url=http://localhost/api/v1/notification 6 | clients.payment.url=http://localhost/api/v1/payment 7 | clients.apiKey-manager.url=http://localhost/api/v1/apiKey-manager/api-keys 8 | internal.api-key=internal-service-key 9 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class PaymentConstant { 7 | public static final String PAYMENT_URI_REST_API = "/api/v1/payments"; 8 | public static final String PAYMENT_NOT_FOUND = "Payment with ID %d not found"; 9 | public static final String PAYMENT_CREATE_ERROR = "Failed to create payment: %s"; 10 | public static final String NO_PAYMENTS_FOUND = "No payments found"; 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Build java microservices with maven 2 | 3 | name: Build - CI 4 | 5 | #on: 6 | # pull_request: 7 | # branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - uses: actions/checkout@v3 15 | - name: Set UP JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | cache: maven 21 | 22 | - name: Maven Clean Package 23 | run: mvn clean package 24 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/ApiKeyManagerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano; 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 ApiKeyManagerApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(ApiKeyManagerApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/payload/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.payload; 2 | 3 | public enum ErrorCode { 4 | RESOURCE_NOT_FOUND("ERR_001"), 5 | VALIDATION_FAILED("ERR_002"), 6 | DUPLICATE_RESOURCE("ERR_003"), 7 | BAD_REQUEST("ERR_004"), 8 | INTERNAL_ERROR("ERR_005"); 9 | 10 | private final String code; 11 | 12 | ErrorCode(String code) { 13 | this.code = code; 14 | } 15 | 16 | public String getCode() { 17 | return code; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.MappingTarget; 5 | 6 | import java.util.List; 7 | 8 | @Mapper(componentModel = "spring") 9 | public interface ProductMapper { 10 | ProductDTO toDTO(ProductEntity entity); 11 | ProductEntity toEntity(ProductDTO dto); 12 | List toListDTO(List entities); 13 | void updateProductFromDTO(ProductDTO dto, @MappingTarget ProductEntity entity); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/exceptionhandler/core/ValidationException.java: -------------------------------------------------------------------------------- 1 | package dev.nano.exceptionhandler.core; 2 | 3 | import dev.nano.exceptionhandler.payload.ValidationError; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | public class ValidationException extends BaseException { 10 | private final List errors; 11 | 12 | public ValidationException(String message, List errors) { 13 | super(message); 14 | this.errors = errors; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /common/src/main/resources/application-openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 2 | title: Microservices API Documentation 3 | version: 1.0 4 | description: Documentation for all microservices endpoints 5 | contact: 6 | name: Adnane MILIARI 7 | email: miliari.adnane@gmail.com 8 | url: https://github.com/miliariadnane 9 | license: 10 | name: Apache 2.0 11 | url: http://www.apache.org/licenses/LICENSE-2.0.html 12 | external-docs: 13 | description: Project Documentation 14 | url: https://github.com/miliariadnane/demo-microservices 15 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.MappingTarget; 5 | 6 | import java.util.List; 7 | 8 | @Mapper(componentModel = "spring") 9 | public interface CustomerMapper { 10 | CustomerDTO toDTO(CustomerEntity entity); 11 | CustomerEntity toEntity(CustomerDTO dto); 12 | List toListDTO(List listEntity); 13 | void updateCustomerFromDTO(CustomerDTO dto, @MappingTarget CustomerEntity entity); 14 | } 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class CustomerConstant { 7 | public static final String CUSTOMER_URI_REST_API = "/api/v1/customers"; 8 | public static final String CUSTOMER_NOT_FOUND = "Customer with ID %d not found"; 9 | public static final String CUSTOMER_EMAIL_EXISTS = "Customer with email %s already exists"; 10 | public static final String NO_CUSTOMERS_FOUND = "No customers found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/product/ProductClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.product; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | 7 | @FeignClient(name = "product", url = "${clients.product.url}") 8 | public interface ProductClient { 9 | 10 | @GetMapping(path = "/api/v1/products/{productId}") 11 | ProductResponse getProduct(@PathVariable("productId") Long productId); 12 | } 13 | -------------------------------------------------------------------------------- /eureka-server/src/main/java/dev/nano/eurekaserver/EurekaServerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.eurekaserver; 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 EurekaServerApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(EurekaServerApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | 7 | @FeignClient(name = "payment", url = "${clients.payment.url}") 8 | public interface PaymentClient { 9 | 10 | @PostMapping(path = "/api/v1/payments/make-new-payment") 11 | PaymentResponse createPayment(@RequestBody PaymentRequest payment); 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/customer/CustomerClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.customer; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | 7 | @FeignClient(name = "customer", url = "${clients.customer.url}") 8 | public interface CustomerClient { 9 | 10 | @GetMapping(path = "/api/v1/customers/{customerId}") 11 | CustomerResponse getCustomer(@PathVariable("customerId") Long customerId); 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.SuperBuilder; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @SuperBuilder 12 | public class NotificationRequest { 13 | private Long customerId; 14 | private String customerName; 15 | private String customerEmail; 16 | private String sender; 17 | private String message; 18 | } 19 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,--.,--. ,--. 2 | ,--,--. ,---. `--'| .' /,---. ,--. ,--.,-----.,--,--,--. ,--,--.,--,--, ,--,--. ,---. ,---. ,--.--. 3 | ' ,-. || .-. |,--.| . '| .-. : \ ' / '-----'| |' ,-. || \' ,-. || .-. || .-. :| .--' 4 | \ '-' || '-' '| || |\ \ --. \ ' | | | |\ '-' || || |\ '-' |' '-' '\ --.| | 5 | `--`--'| |-' `--'`--' '--'`----'.-' / `--`--`--' `--`--'`--''--' `--`--'.`- / `----'`--' 6 | `--' `---' `---' 7 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class NotificationConstant { 7 | public static final String NOTIFICATION_URI_REST_API = "/api/v1/notifications"; 8 | public static final String NOTIFICATION_NOT_FOUND = "Notification with ID %d not found"; 9 | public static final String NOTIFICATION_SEND_ERROR = "Failed to send notification: %s"; 10 | public static final String NO_NOTIFICATIONS_FOUND = "No notifications found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | 7 | @FeignClient(name = "notification", url = "${clients.notification.url}") 8 | public interface NotificationClient { 9 | 10 | @PostMapping(path = "/api/v1/notifications/send") 11 | NotificationResponse sendNotification(@RequestBody NotificationRequest notificationRequest); 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/cache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.cache; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.EnableCaching; 5 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @EnableCaching 11 | public class CacheConfig { 12 | @Bean 13 | public CacheManager cacheManager() { 14 | return new ConcurrentMapCacheManager("apikey-authorizations"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /feign-clients/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | feign-clients 13 | 14 | 15 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class ProductConstant { 7 | public static final String PRODUCT_URI_REST_API = "/api/v1/products"; 8 | public static final String PRODUCT_NOT_FOUND = "Product with ID %d not found"; 9 | public static final String PRODUCT_CREATE_ERROR = "Failed to create product: %s"; 10 | public static final String PRODUCT_DELETE_ERROR = "Failed to delete product: %s"; 11 | public static final String NO_PRODUCTS_FOUND = "No products found"; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /product/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO product (id, name, image, price, available_quantity) 2 | VALUES 3 | (nextval('product_sequence'), 'MacBook Pro M3', 'https://picsum.photos/id/1/200/200', 1999, 50), 4 | (nextval('product_sequence'), 'iPhone 15 Pro', 'https://picsum.photos/id/2/200/200', 1299, 100), 5 | (nextval('product_sequence'), 'AirPods Pro', 'https://picsum.photos/id/3/200/200', 249, 200), 6 | (nextval('product_sequence'), 'iPad Air', 'https://picsum.photos/id/4/200/200', 599, 75), 7 | (nextval('product_sequence'), 'Apple Watch Series 9', 'https://picsum.photos/id/5/200/200', 499, 120); 8 | -------------------------------------------------------------------------------- /customer/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO customer (id, name, email, phone, address) 2 | VALUES (nextval('customer_sequence'), 'John Smith', 'john.smith@gmail.com', '+1234567890', '123 Main St, New York, NY'), 3 | (nextval('customer_sequence'), 'Emma Wilson', 'emma.wilson@outlook.com', '+1987654321', 4 | '456 Park Ave, London, UK'), 5 | (nextval('customer_sequence'), 'Mohammed Ali', 'mohammed.ali@yahoo.com', '+212661234567', 6 | 'Marina Street, Casablanca, Morocco'), 7 | (nextval('customer_sequence'), 'Sarah Chen', 'sarah.chen@gmail.com', '+8613912345678', 8 | '789 Nanjing Road, Shanghai, China'); 9 | -------------------------------------------------------------------------------- /notification/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8004 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: notification 9 | config: 10 | import: 11 | - "classpath:shared-application-${spring.profiles.active}.yml" 12 | - "classpath:clients-${spring.profiles.active}.properties" 13 | - "classpath:amqp-${spring.profiles.active}.properties" 14 | 15 | #RabbitMQ 16 | rabbitmq: 17 | exchange: 18 | internal: internal.exchange 19 | queue: 20 | notification: notification.queue 21 | routing-key: 22 | internal-notification: internal.notification.routing-key 23 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | 8 | @SpringBootApplication 9 | @EnableDiscoveryClient 10 | @EnableFeignClients( 11 | basePackages = "dev.nano.clients" 12 | 13 | ) 14 | public class GatewayApplication { 15 | public static void main(String[] args) { 16 | SpringApplication.run(GatewayApplication.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/volume.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: postgres-pc-volume 5 | labels: 6 | type: local # local volume 7 | app: postgres 8 | spec: 9 | storageClassName: manual 10 | capacity: 11 | storage: 5Gi 12 | accessModes: 13 | - ReadWriteMany 14 | hostPath: 15 | path: /mnt/data # the mount path of the local volume 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolumeClaim 19 | metadata: 20 | name: postgres-pc-volume-claim 21 | labels: 22 | app: postgres 23 | spec: 24 | storageClassName: manual 25 | accessModes: 26 | - ReadWriteMany 27 | resources: 28 | requests: 29 | storage: 5Gi 30 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/ingress/readme.MD: -------------------------------------------------------------------------------- 1 | ## Ingress 2 | 3 | * Kubernetes Ingress is an API object that provides routing rules to manage external users access to the services in a Kubernetes cluster. 4 | * Ingress can provide load balancing, SSL termination and name-based virtual hosting. 5 | * We must add ingress in our kubernetes minikube cluster to access our services from outside the cluster by specifying the customized path and host name for each microservice, also to manage load balancing. 6 | 7 | To read more about ingress, you can check k8s documentation : 8 | [Ingress k8s doc](https://kubernetes.io/docs/concepts/services-networking/ingress/) 9 | 10 | ![Ingress](../../../../docs/images/ingress.jpg) 11 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class PaymentDTO { 16 | private Long id; 17 | 18 | @NotNull(message = "Customer ID is required") 19 | private Long customerId; 20 | 21 | @NotNull(message = "Order ID is required") 22 | private Long orderId; 23 | 24 | private LocalDateTime createAt; 25 | 26 | private PaymentStatus status; 27 | } 28 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/rbac.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rabbitmq 6 | --- 7 | kind: Role 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | metadata: 10 | name: rabbitmq-peer-discovery-rbac 11 | rules: 12 | - apiGroups: [""] 13 | resources: ["endpoints"] 14 | verbs: ["get"] 15 | - apiGroups: [""] 16 | resources: ["events"] 17 | verbs: ["create"] 18 | --- 19 | kind: RoleBinding 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | metadata: 22 | name: rabbitmq-peer-discovery-rbac 23 | subjects: 24 | - kind: ServiceAccount 25 | name: rabbitmq 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: rabbitmq-peer-discovery-rbac 30 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/rbac.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rabbitmq 6 | --- 7 | kind: Role 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | metadata: 10 | name: rabbitmq-peer-discovery-rbac 11 | rules: 12 | - apiGroups: [""] 13 | resources: ["endpoints"] 14 | verbs: ["get"] 15 | - apiGroups: [""] 16 | resources: ["events"] 17 | verbs: ["create"] 18 | --- 19 | kind: RoleBinding 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | metadata: 22 | name: rabbitmq-peer-discovery-rbac 23 | subjects: 24 | - kind: ServiceAccount 25 | name: rabbitmq 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: rabbitmq-peer-discovery-rbac 30 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/application-docker.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8761 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: eureka-server 9 | 10 | #Management 11 | management: 12 | observations: 13 | key-values: 14 | application: ${spring.application.name} 15 | tracing: 16 | sampling: 17 | probability: 1.0 18 | enabled: true 19 | zipkin: 20 | tracing: 21 | endpoint: http://zipkin:9411/api/v2/spans 22 | endpoints: 23 | web: 24 | exposure: 25 | include: "*" 26 | endpoint: 27 | health: 28 | show-details: always 29 | 30 | #Eureka-Server 31 | eureka: 32 | client: 33 | fetch-registry: false 34 | register-with-eureka: false 35 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | demo-microservices 5 | dev.nano 6 | 1.0-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | common 11 | 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-web 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/config/InternalApiKeyFeignConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.config; 2 | 3 | import feign.RequestInterceptor; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class InternalApiKeyFeignConfig { 10 | 11 | @Value("${internal.api-key}") 12 | private String internalApiKey; 13 | 14 | private static final String API_KEY_HEADER = "X-API-KEY"; 15 | 16 | @Bean 17 | public RequestInterceptor internalApiKeyRequestInterceptor() { 18 | return requestTemplate -> requestTemplate.header(API_KEY_HEADER, internalApiKey); 19 | } 20 | } -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/email/DefaultEmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.email; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | @Profile("!eks") 9 | @Slf4j 10 | public class DefaultEmailService implements EmailService { 11 | @Override 12 | public void send(String to, String content, String subject) { 13 | if (to == null || to.isBlank()) { 14 | log.warn("Email sending skipped: recipient address is null or empty."); 15 | return; 16 | } 17 | log.info("Simulated email sent to: {}, subject: {}, content: {}", to, subject, content); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import dev.nano.clients.order.OrderRequest; 4 | import dev.nano.clients.order.OrderResponse; 5 | import dev.nano.clients.payment.PaymentRequest; 6 | import dev.nano.clients.payment.PaymentResponse; 7 | 8 | import java.util.List; 9 | 10 | public interface CustomerService { 11 | CustomerDTO getCustomer(Long id); 12 | List getAllCustomers(); 13 | CustomerDTO createCustomer(CustomerDTO customer); 14 | CustomerDTO updateCustomer(Long id, CustomerDTO customer); 15 | void deleteCustomer(Long id); 16 | OrderResponse customerOrders(OrderRequest orderRequest); 17 | PaymentResponse customerPayment(PaymentRequest paymentRequest); 18 | } 19 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.ComponentScan; 8 | 9 | @SpringBootApplication 10 | @EnableDiscoveryClient 11 | @EnableFeignClients(basePackages = "dev.nano.clients") 12 | @ComponentScan(basePackages = "dev.nano") 13 | public class ProductApplication { 14 | public static void main(String[] args) { 15 | SpringApplication.run(ProductApplication.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/grafana/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: grafana 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: grafana 10 | template: 11 | metadata: 12 | labels: 13 | app: grafana 14 | spec: 15 | containers: 16 | - name: grafana 17 | image: grafana/grafana:latest 18 | ports: 19 | - containerPort: 3000 20 | volumeMounts: 21 | - name: grafana-datasources-volume 22 | mountPath: /etc/grafana/provisioning/datasources 23 | volumes: 24 | - name: grafana-datasources-volume 25 | configMap: 26 | name: grafana-datasources 27 | restartPolicy: Always -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-kube.yml: -------------------------------------------------------------------------------- 1 | management: 2 | zipkin: 3 | tracing: 4 | endpoint: http://zipkin:9411/api/v2/spans 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://postgres:5432/${spring.application.name} 9 | username: ${DB_USERNAME} 10 | password: ${DB_PASSWORD} 11 | rabbitmq: 12 | addresses: rabbitmq:5672 13 | jpa: 14 | hibernate: 15 | ddl-auto: create 16 | sql: 17 | init: 18 | mode: never 19 | security: 20 | oauth2: 21 | resourceserver: 22 | jwt: 23 | issuer-uri: http://localhost/realms/demo-realm 24 | 25 | eureka: 26 | client: 27 | enabled: false 28 | 29 | jwt: 30 | auth: 31 | converter: 32 | principle-attribute: preferred_username 33 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/prometheus/cluster-role.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: prometheus 5 | rules: 6 | - apiGroups: [""] 7 | resources: 8 | - nodes 9 | - services 10 | - endpoints 11 | - pods 12 | verbs: ["get", "list", "watch"] 13 | - apiGroups: 14 | - extensions 15 | resources: 16 | - ingresses 17 | verbs: ["get", "list", "watch"] 18 | - nonResourceURLs: ["/metrics"] 19 | verbs: ["get"] 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: ClusterRoleBinding 23 | metadata: 24 | name: prometheus 25 | roleRef: 26 | apiGroup: rbac.authorization.k8s.io 27 | kind: ClusterRole 28 | name: prometheus 29 | subjects: 30 | - kind: ServiceAccount 31 | name: default 32 | namespace: default -------------------------------------------------------------------------------- /amqp/src/main/java/dev/nano/amqp/RabbitMQProducer.java: -------------------------------------------------------------------------------- 1 | package dev.nano.amqp; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.amqp.core.AmqpTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @AllArgsConstructor @Slf4j 10 | public class RabbitMQProducer { 11 | 12 | private final AmqpTemplate amqpTemplate; 13 | 14 | public void publish(String exchange, String routingKey, Object payload) { 15 | log.info("Publishing to {} using routingKey {} with payload {}", exchange, routingKey, payload); 16 | amqpTemplate.convertAndSend(exchange, routingKey, payload); 17 | log.info("Published to {} using routingKey {}. Payload: {}", exchange, routingKey, payload); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.context.annotation.ComponentScan; 7 | 8 | @SpringBootApplication( 9 | scanBasePackages = { 10 | "dev.nano.notification", 11 | "dev.nano.amqp", 12 | } 13 | ) 14 | @EnableDiscoveryClient 15 | @ComponentScan(basePackages = "dev.nano") 16 | public class NotificationApplication { 17 | public static void main(String[] args) { 18 | SpringApplication.run(NotificationApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /amqp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | amqp 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-amqp 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/zipkin/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: zipkin 5 | labels: 6 | app: zipkin 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: zipkin 11 | serviceName: zipkin 12 | replicas: 1 13 | template: 14 | metadata: 15 | name: zipkin 16 | labels: 17 | app: zipkin 18 | spec: 19 | containers: 20 | - name: zipkin 21 | image: openzipkin/zipkin 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 9411 25 | protocol: TCP 26 | resources: 27 | requests: 28 | cpu: 100m 29 | memory: 256Mi 30 | limits: 31 | cpu: 200m 32 | memory: 256Mi 33 | restartPolicy: Always 34 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/zipkin/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: zipkin 5 | labels: 6 | app: zipkin 7 | spec: 8 | serviceName: zipkin 9 | replicas: 1 10 | template: 11 | metadata: 12 | name: zipkin 13 | labels: 14 | app: zipkin 15 | spec: 16 | containers: 17 | - name: zipkin 18 | image: openzipkin/zipkin 19 | imagePullPolicy: Always 20 | ports: 21 | - containerPort: 9411 22 | protocol: TCP 23 | resources: 24 | requests: 25 | cpu: 100m 26 | memory: 256Mi 27 | limits: 28 | cpu: 200m 29 | memory: 256Mi 30 | 31 | restartPolicy: Always 32 | selector: 33 | matchLabels: 34 | app: zipkin 35 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Data 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class OrderDTO { 17 | private Long id; 18 | 19 | @NotNull(message = "Customer ID is required") 20 | private Long customerId; 21 | 22 | @NotNull(message = "Product ID is required") 23 | private Long productId; 24 | 25 | @NotNull(message = "Amount is required") 26 | @Min(value = 1, message = "Amount must be greater than 0") 27 | private double amount; 28 | 29 | private LocalDateTime createAt; 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-eks.yml: -------------------------------------------------------------------------------- 1 | management: 2 | zipkin: 3 | tracing: 4 | endpoint: http://zipkin:9411/api/v2/spans 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://postgres:5432/${spring.application.name} 9 | username: miliariadnane 10 | password: password 11 | rabbitmq: 12 | addresses: rabbitmq:5672 13 | sql: 14 | init: 15 | mode: always 16 | data-locations: classpath*:db/data.sql 17 | platform: postgresql 18 | security: 19 | oauth2: 20 | resourceserver: 21 | jwt: 22 | issuer-uri: http://keycloak:8080/realms/demo-realm 23 | 24 | eureka: 25 | client: 26 | service-url: 27 | defaultZone: http://eureka-server:8761/eureka 28 | enabled: false 29 | 30 | jwt: 31 | auth: 32 | converter: 33 | principle-attribute: preferred_username 34 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import dev.nano.clients.config.InternalApiKeyFeignConfig; 4 | import org.springframework.cloud.openfeign.FeignClient; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | 10 | @FeignClient(name = "order", url = "${clients.order.url}", configuration = InternalApiKeyFeignConfig.class) 11 | public interface OrderClient { 12 | @GetMapping(path = "/api/v1/orders/{orderId}") 13 | OrderResponse getOrder(@PathVariable("orderId") Long orderId); 14 | 15 | @PostMapping(path = "/api/v1/orders/add") 16 | OrderResponse createOrder(@RequestBody OrderRequest orderRequest); 17 | } 18 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/rabbitmq/NotificationConsumer.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.rabbitmq; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | import dev.nano.notification.NotificationService; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @AllArgsConstructor @Slf4j 12 | public class NotificationConsumer { 13 | 14 | private final NotificationService notificationService; 15 | 16 | @RabbitListener(queues = "${rabbitmq.queue.notification}") 17 | public void consumer(NotificationRequest notificationRequest) { 18 | log.info("Consumed {} from queue", notificationRequest); 19 | notificationService.sendNotification(notificationRequest); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.ComponentScan; 8 | 9 | @SpringBootApplication( 10 | scanBasePackages = { 11 | "dev.nano.order", 12 | "dev.nano.amqp" 13 | } 14 | ) 15 | @EnableDiscoveryClient 16 | @EnableFeignClients( 17 | basePackages = "dev.nano.clients" 18 | ) 19 | @ComponentScan(basePackages = "dev.nano") 20 | public class OrderApplication { 21 | public static void main(String[] args) { 22 | SpringApplication.run(OrderApplication.class, args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | 10 | @Repository 11 | public interface ProductRepository extends JpaRepository { 12 | 13 | @Query(value="SELECT * FROM product", nativeQuery=true) 14 | Page findAllProducts(Pageable pageableRequest); 15 | 16 | @Query(value="SELECT * FROM product p WHERE (p.name LIKE CONCAT('%', :search, '%')) ", nativeQuery=true) 17 | Page findAllProductsByCriteria(Pageable pageableRequest, @Param("search") String search); 18 | } 19 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8761 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: eureka-server 9 | 10 | #Management 11 | management: 12 | observations: 13 | key-values: 14 | application: ${spring.application.name} 15 | tracing: 16 | sampling: 17 | probability: 1.0 18 | enabled: true 19 | zipkin: 20 | tracing: 21 | endpoint: http://localhost:9411/api/v2/spans 22 | endpoints: 23 | web: 24 | exposure: 25 | include: "*" 26 | endpoint: 27 | health: 28 | show-details: always 29 | 30 | #Eureka-Server 31 | eureka: 32 | client: 33 | fetch-registry: false 34 | register-with-eureka: false 35 | 36 | #Logging 37 | logging: 38 | pattern: 39 | correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" 40 | level: "%5p ${logging.pattern.correlation} %c{1.} : %m%n" 41 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/apiKeyManager/apiKey/ApiKeyManagerClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.apiKeyManager.apiKey; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.PutMapping; 7 | 8 | @FeignClient(name = "apiKey-manager", url = "${clients.apiKey-manager.url}") 9 | public interface ApiKeyManagerClient { 10 | 11 | @GetMapping("{apiKey}/applications/{applicationName}/authorization") 12 | boolean isKeyAuthorizedForApplication( 13 | @PathVariable("apiKey") String apiKey, 14 | @PathVariable("applicationName") String applicationName); 15 | 16 | @PutMapping("/api/v1/apiKey-manager/api-keys/{apiKey}/revoke") 17 | void revokeKey(@PathVariable("apiKey") String apiKey); 18 | } 19 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: Prometheus 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: prometheus 6 | app.kubernetes.io/instance: k8s 7 | app.kubernetes.io/name: prometheus 8 | app.kubernetes.io/part-of: kube-prometheus 9 | app.kubernetes.io/version: 2.32.1 10 | name: applications 11 | namespace: monitoring 12 | spec: 13 | image: quay.io/prometheus/prometheus:v2.32.1 14 | nodeSelector: 15 | kubernetes.io/os: linux 16 | replicas: 1 17 | resources: 18 | requests: 19 | memory: 400Mi 20 | ruleSelector: {} 21 | securityContext: 22 | fsGroup: 2000 23 | runAsNonRoot: true 24 | runAsUser: 1000 25 | serviceAccountName: prometheus-k8s 26 | serviceMonitorNamespaceSelector: 27 | matchLabels: 28 | kubernetes.io/metadata.name: default 29 | serviceMonitorSelector: {} 30 | version: 2.32.1 31 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.ComponentScan; 8 | 9 | @SpringBootApplication( 10 | scanBasePackages = { 11 | "dev.nano.customer", 12 | "dev.nano.amqp", 13 | "dev.nano.clients" 14 | } 15 | ) 16 | @EnableFeignClients( 17 | basePackages = "dev.nano.clients" 18 | ) 19 | @EnableDiscoveryClient 20 | @ComponentScan(basePackages = "dev.nano") 21 | public class CustomerApplication { 22 | public static void main(String[] args) { 23 | SpringApplication.run(CustomerApplication.class, args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class CustomerDTO { 17 | private Long id; 18 | 19 | @NotBlank(message = "Name is required") 20 | @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") 21 | private String name; 22 | 23 | @NotBlank(message = "Email is required") 24 | @Size(max = 255, message = "Email cannot exceed 255 characters") 25 | private String email; 26 | 27 | private String phone; 28 | 29 | @Size(max = 255, message = "Address cannot exceed 255 characters") 30 | private String address; 31 | } 32 | -------------------------------------------------------------------------------- /notification/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO notification (id, customer_id, customer_name, customer_email, sender, message, sent_at) 2 | VALUES (nextval('notification_sequence'), 1, 'John Smith', 'john.smith@gmail.com', 'NanoShop', 3 | 'Your order #1 has been confirmed', CURRENT_TIMESTAMP), 4 | (nextval('notification_sequence'), 1, 'John Smith', 'john.smith@gmail.com', 'NanoShop', 5 | 'Your payment for order #1 has been processed', CURRENT_TIMESTAMP), 6 | (nextval('notification_sequence'), 2, 'Emma Wilson', 'emma.wilson@outlook.com', 'NanoShop', 7 | 'Your order #3 has been shipped', CURRENT_TIMESTAMP), 8 | (nextval('notification_sequence'), 3, 'Mohammed Ali', 'mohammed.ali@yahoo.com', 'NanoShop', 9 | 'Payment reminder for order #4', CURRENT_TIMESTAMP), 10 | (nextval('notification_sequence'), 4, 'Sarah Chen', 'sarah.chen@gmail.com', 'NanoShop', 11 | 'Thank you for your purchase! Order #5 confirmed', CURRENT_TIMESTAMP); 12 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @SpringBootApplication( 11 | scanBasePackages = { 12 | "dev.nano.payment", 13 | "dev.nano.amqp" 14 | } 15 | ) 16 | @EnableDiscoveryClient 17 | @EnableFeignClients( 18 | basePackages = "dev.nano.clients" 19 | ) 20 | @ComponentScan(basePackages = "dev.nano") 21 | @EnableScheduling 22 | public class PaymentApplication { 23 | public static void main(String[] args) { 24 | SpringApplication.run(PaymentApplication.class, args); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /eureka-server/src/main/java/dev/nano/eurekaserver/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.eurekaserver.config; 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.config.annotation.web.configuration.EnableWebSecurity; 7 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 8 | import org.springframework.security.web.SecurityFilterChain; 9 | 10 | @Configuration 11 | @EnableWebSecurity 12 | public class SecurityConfig { 13 | 14 | @Bean 15 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 16 | http 17 | .csrf(AbstractHttpConfigurer::disable) 18 | .authorizeHttpRequests(authorize -> authorize 19 | .anyRequest().permitAll() 20 | ); 21 | return http.build(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/prometheus/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: prometheus 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: prometheus 10 | template: 11 | metadata: 12 | labels: 13 | app: prometheus 14 | spec: 15 | containers: 16 | - name: prometheus 17 | image: prom/prometheus:latest 18 | args: 19 | - "--config.file=/etc/prometheus/prometheus.yml" 20 | - "--storage.tsdb.path=/prometheus" 21 | ports: 22 | - containerPort: 9090 23 | volumeMounts: 24 | - name: prometheus-config-volume 25 | mountPath: /etc/prometheus 26 | - name: prometheus-storage-volume 27 | mountPath: /prometheus 28 | volumes: 29 | - name: prometheus-config-volume 30 | configMap: 31 | name: prometheus-config 32 | - name: prometheus-storage-volume 33 | emptyDir: {} 34 | restartPolicy: Always -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/feign/FeignClientInterceptor.java: -------------------------------------------------------------------------------- 1 | package dev.nano.feign; 2 | 3 | import feign.RequestInterceptor; 4 | import feign.RequestTemplate; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.context.request.RequestContextHolder; 7 | import org.springframework.web.context.request.ServletRequestAttributes; 8 | 9 | @Component 10 | public class FeignClientInterceptor implements RequestInterceptor { 11 | 12 | private static final String AUTHORIZATION_HEADER = "Authorization"; 13 | 14 | @Override 15 | public void apply(RequestTemplate template) { 16 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 17 | if (attributes != null) { 18 | String authorizationHeader = attributes.getRequest().getHeader(AUTHORIZATION_HEADER); 19 | if (authorizationHeader != null && !authorizationHeader.isEmpty()) { 20 | template.header(AUTHORIZATION_HEADER, authorizationHeader); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/swagger/OpenAPIProperties.java: -------------------------------------------------------------------------------- 1 | package dev.nano.swagger; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | @ConfigurationProperties(prefix = "openapi") 8 | @Getter @Setter 9 | public class OpenAPIProperties { 10 | private String title; 11 | private String version; 12 | private String description; 13 | private Contact contact = new Contact(); 14 | private License license = new License(); 15 | private ExternalDocs externalDocs = new ExternalDocs(); 16 | 17 | @Getter 18 | @Setter 19 | public static class Contact { 20 | private String name; 21 | private String email; 22 | private String url; 23 | } 24 | 25 | @Getter 26 | @Setter 27 | public static class License { 28 | private String name; 29 | private String url; 30 | } 31 | 32 | @Getter 33 | @Setter 34 | public static class ExternalDocs { 35 | private String description; 36 | private String url; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres 5 | labels: 6 | app: postgres 7 | spec: 8 | serviceName: postgres 9 | replicas: 1 10 | template: 11 | metadata: 12 | name: postgres 13 | labels: 14 | app: postgres 15 | spec: 16 | volumes: 17 | - name: postgres 18 | persistentVolumeClaim: 19 | claimName: postgres-pc-volume-claim 20 | containers: 21 | - name: postgres 22 | image: postgres 23 | imagePullPolicy: IfNotPresent 24 | volumeMounts: 25 | - mountPath: "/var/lib/postgresql/data" 26 | name: postgres 27 | envFrom: 28 | - configMapRef: 29 | name: postgres-config 30 | resources: 31 | requests: 32 | cpu: 100m 33 | memory: 256Mi 34 | limits: 35 | cpu: 500m 36 | memory: 512Mi 37 | restartPolicy: Always 38 | selector: 39 | matchLabels: 40 | app: postgres 41 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | @Data 15 | @Builder 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class NotificationDTO { 19 | private Long id; 20 | 21 | @NotNull(message = "Customer ID is required") 22 | private Long customerId; 23 | 24 | @NotBlank(message = "Customer name is required") 25 | private String customerName; 26 | 27 | @NotBlank(message = "Customer email is required") 28 | @Email(message = "Invalid email format") 29 | private String customerEmail; 30 | 31 | @NotBlank(message = "Sender is required") 32 | private String sender; 33 | 34 | @NotBlank(message = "Message is required") 35 | private String message; 36 | 37 | private LocalDateTime sentAt; 38 | } 39 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/KeycloakRoleConverter.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | import org.springframework.security.oauth2.jwt.Jwt; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | 13 | public class KeycloakRoleConverter implements Converter> { 14 | 15 | @Override 16 | public Collection convert(Jwt jwt) { 17 | Map realmAccess = (Map) jwt.getClaims().get("realm_access"); 18 | 19 | if (realmAccess == null || realmAccess.isEmpty()) { 20 | return List.of(); 21 | } 22 | 23 | return ((List) realmAccess.get("roles")).stream() 24 | .map(roleName -> "ROLE_" + roleName) 25 | .map(SimpleGrantedAuthority::new) 26 | .collect(Collectors.toList()); 27 | } 28 | } -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: order 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: order 10 | template: 11 | metadata: 12 | labels: 13 | app: order 14 | spec: 15 | containers: 16 | - name: order 17 | image: miliariadnane/order:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8003 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payment 10 | template: 11 | metadata: 12 | labels: 13 | app: payment 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/payment:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8005 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: product 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: product 10 | template: 11 | metadata: 12 | labels: 13 | app: product 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/product:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8002 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: customer 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: customer 10 | template: 11 | metadata: 12 | labels: 13 | app: customer 14 | spec: 15 | containers: 16 | - name: customer 17 | image: miliariadnane/customer:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8001 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import org.hibernate.validator.constraints.URL; 12 | 13 | @Data 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class ProductDTO { 18 | private Long id; 19 | 20 | @NotBlank(message = "Product name is required") 21 | @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") 22 | private String name; 23 | 24 | @NotNull(message = "Price is required") 25 | @Min(value = 0, message = "Price must be greater than or equal to 0") 26 | private Integer price; 27 | 28 | @URL(message = "Invalid image URL") 29 | private String image; 30 | 31 | @NotNull(message = "Available quantity is required") 32 | @Min(value = 0, message = "Available quantity must be greater than or equal to 0") 33 | private Integer availableQuantity; 34 | } 35 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: notification 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: notification 10 | template: 11 | metadata: 12 | labels: 13 | app: notification 14 | spec: 15 | containers: 16 | - name: notification 17 | image: miliariadnane/notification:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8004 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | 8 | import java.util.Optional; 9 | 10 | public interface ApiKeyRepository extends JpaRepository { 11 | @Query(""" 12 | SELECT ak FROM ApiKeyEntity ak 13 | INNER JOIN ApplicationEntity ap 14 | ON ak.id = ap.apiKey.id 15 | WHERE ak.key = :key 16 | AND ap.applicationName = :applicationName 17 | """) 18 | Optional findByKeyAndApplicationName(@Param("key") String key, @Param("applicationName") ApplicationName applicationName); 19 | 20 | @Query(""" 21 | SELECT 22 | CASE WHEN COUNT(ak) > 0 23 | THEN TRUE 24 | ELSE FALSE 25 | END 26 | FROM ApiKeyEntity ak 27 | WHERE ak.key = :key 28 | """) 29 | boolean doesKeyExists(String key); 30 | 31 | @Query("SELECT ak FROM ApiKeyEntity ak WHERE ak.key = :key") 32 | Optional findByKey(String key); 33 | } 34 | -------------------------------------------------------------------------------- /k8s/minikube/services/payment/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payment 10 | template: 11 | metadata: 12 | labels: 13 | app: payment 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8005" 18 | spec: 19 | containers: 20 | - name: payment 21 | image: miliariadnane/payment:latest 22 | imagePullPolicy: IfNotPresent 23 | ports: 24 | - containerPort: 8005 25 | env: 26 | - name: SPRING_PROFILES_ACTIVE 27 | value: kube 28 | - name: DB_USERNAME 29 | valueFrom: 30 | secretKeyRef: 31 | name: postgres-secret 32 | key: DB_USERNAME 33 | - name: DB_PASSWORD 34 | valueFrom: 35 | secretKeyRef: 36 | name: postgres-secret 37 | key: DB_PASSWORD 38 | resources: 39 | requests: 40 | cpu: "200m" 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/minikube/services/product/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: product 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: product 10 | template: 11 | metadata: 12 | labels: 13 | app: product 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8002" 18 | spec: 19 | containers: 20 | - name: product 21 | image: miliariadnane/product:latest 22 | imagePullPolicy: IfNotPresent 23 | ports: 24 | - containerPort: 8002 25 | env: 26 | - name: SPRING_PROFILES_ACTIVE 27 | value: kube 28 | - name: DB_USERNAME 29 | valueFrom: 30 | secretKeyRef: 31 | name: postgres-secret 32 | key: DB_USERNAME 33 | - name: DB_PASSWORD 34 | valueFrom: 35 | secretKeyRef: 36 | name: postgres-secret 37 | key: DB_PASSWORD 38 | resources: 39 | requests: 40 | cpu: "200m" 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/minikube/services/apikey-manager/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: apikey-manager 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: apikey-manager 10 | template: 11 | metadata: 12 | labels: 13 | app: apikey-manager 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8006" 18 | spec: 19 | containers: 20 | - name: apikey-manager 21 | image: miliariadnane/apikey-manager:latest 22 | imagePullPolicy: IfNotPresent 23 | ports: 24 | - containerPort: 8006 25 | env: 26 | - name: SPRING_PROFILES_ACTIVE 27 | value: kube 28 | - name: DB_USERNAME 29 | valueFrom: 30 | secretKeyRef: 31 | name: postgres-secret 32 | key: DB_USERNAME 33 | - name: DB_PASSWORD 34 | valueFrom: 35 | secretKeyRef: 36 | name: postgres-secret 37 | key: DB_PASSWORD 38 | resources: 39 | requests: 40 | cpu: "200m" 41 | restartPolicy: Always -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/prometheus/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: prometheus-config 5 | data: 6 | prometheus.yml: | 7 | global: 8 | scrape_interval: 15s 9 | scrape_configs: 10 | - job_name: 'kubernetes-pods' 11 | kubernetes_sd_configs: 12 | - role: pod 13 | relabel_configs: 14 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] 15 | action: keep 16 | regex: true 17 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] 18 | action: replace 19 | target_label: __metrics_path__ 20 | regex: (.+) 21 | - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] 22 | action: replace 23 | regex: ([^:]+)(?::\d+)?;(\d+) 24 | replacement: $1:$2 25 | target_label: __address__ 26 | - action: labelmap 27 | regex: __meta_kubernetes_pod_label_(.+) 28 | - source_labels: [__meta_kubernetes_namespace] 29 | action: replace 30 | target_label: kubernetes_namespace 31 | - source_labels: [__meta_kubernetes_pod_name] 32 | action: replace 33 | target_label: kubernetes_pod_name -------------------------------------------------------------------------------- /k8s/minikube/services/order/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: order 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: order 10 | template: 11 | metadata: 12 | labels: 13 | app: order 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8003" 18 | spec: 19 | containers: 20 | - name: order 21 | image: miliariadnane/order:latest 22 | imagePullPolicy: IfNotPresent 23 | resources: 24 | requests: 25 | cpu: "200m" 26 | memory: "256Mi" 27 | limits: 28 | memory: "512Mi" 29 | cpu: "500m" 30 | ports: 31 | - containerPort: 8003 32 | env: 33 | - name: SPRING_PROFILES_ACTIVE 34 | value: kube 35 | - name: DB_USERNAME 36 | valueFrom: 37 | secretKeyRef: 38 | name: postgres-secret 39 | key: DB_USERNAME 40 | - name: DB_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: postgres-secret 44 | key: DB_PASSWORD 45 | restartPolicy: Always 46 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import dev.nano.apikey.ApiKeyEntity; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | import jakarta.persistence.*; 10 | 11 | @Entity 12 | @Table(name = "applications") 13 | @Data @SuperBuilder 14 | @NoArgsConstructor @AllArgsConstructor 15 | public class ApplicationEntity { 16 | @Id 17 | @SequenceGenerator( 18 | name = "application_sequence", 19 | sequenceName = "application_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "application_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Integer id; 31 | @Enumerated(EnumType.STRING) 32 | @Column(nullable = false) 33 | private ApplicationName applicationName; 34 | private boolean enabled; 35 | private boolean approved; 36 | private boolean revoked; 37 | @ManyToOne(fetch = FetchType.LAZY) 38 | @JoinColumn( 39 | name = "api_key_id", 40 | referencedColumnName = "id" 41 | ) 42 | private ApiKeyEntity apiKey; 43 | } 44 | -------------------------------------------------------------------------------- /k8s/minikube/services/customer/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: customer 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: customer 10 | template: 11 | metadata: 12 | labels: 13 | app: customer 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8001" 18 | spec: 19 | containers: 20 | - name: customer 21 | image: miliariadnane/customer:latest 22 | imagePullPolicy: IfNotPresent 23 | resources: 24 | requests: 25 | cpu: "200m" 26 | memory: "256Mi" 27 | limits: 28 | memory: "512Mi" 29 | cpu: "500m" 30 | ports: 31 | - containerPort: 8001 32 | env: 33 | - name: SPRING_PROFILES_ACTIVE 34 | value: kube 35 | - name: DB_USERNAME 36 | valueFrom: 37 | secretKeyRef: 38 | name: postgres-secret 39 | key: DB_USERNAME 40 | - name: DB_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: postgres-secret 44 | key: DB_PASSWORD 45 | restartPolicy: Always 46 | -------------------------------------------------------------------------------- /k8s/minikube/services/notification/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: notification 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: notification 10 | template: 11 | metadata: 12 | labels: 13 | app: notification 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/actuator/prometheus" 17 | prometheus.io/port: "8004" 18 | spec: 19 | containers: 20 | - name: notification 21 | image: miliariadnane/notification:latest 22 | imagePullPolicy: IfNotPresent 23 | resources: 24 | requests: 25 | cpu: "200m" 26 | memory: "256Mi" 27 | limits: 28 | memory: "512Mi" 29 | cpu: "500m" 30 | ports: 31 | - containerPort: 8004 32 | env: 33 | - name: SPRING_PROFILES_ACTIVE 34 | value: kube 35 | - name: DB_USERNAME 36 | valueFrom: 37 | secretKeyRef: 38 | name: postgres-secret 39 | key: DB_USERNAME 40 | - name: DB_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: postgres-secret 44 | key: DB_PASSWORD 45 | restartPolicy: Always 46 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Sequences 2 | CREATE SEQUENCE IF NOT EXISTS api_key_sequence START WITH 1 INCREMENT BY 1; 3 | CREATE SEQUENCE IF NOT EXISTS application_sequence START WITH 1 INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS api_keys 6 | ( 7 | id BIGINT DEFAULT nextval('api_key_sequence') PRIMARY KEY, 8 | key VARCHAR (255) UNIQUE NOT NULL, 9 | client VARCHAR(255) UNIQUE NOT NULL, 10 | description TEXT, 11 | created_date TIMESTAMP, 12 | expiration_date TIMESTAMP, 13 | enabled BOOLEAN NOT NULL DEFAULT FALSE, 14 | never_expires BOOLEAN NOT NULL DEFAULT FALSE, 15 | approved BOOLEAN NOT NULL DEFAULT FALSE, 16 | revoked BOOLEAN NOT NULL DEFAULT FALSE 17 | ); 18 | 19 | CREATE TABLE IF NOT EXISTS applications 20 | ( 21 | id INT DEFAULT nextval('application_sequence') PRIMARY KEY, 22 | application_name VARCHAR(255) NOT NULL, 23 | enabled BOOLEAN NOT NULL DEFAULT FALSE, 24 | approved BOOLEAN NOT NULL DEFAULT FALSE, 25 | revoked BOOLEAN NOT NULL DEFAULT FALSE, 26 | api_key_id BIGINT NOT NULL, 27 | CONSTRAINT fk_api_key 28 | FOREIGN KEY (api_key_id) 29 | REFERENCES api_keys (id) 30 | ON DELETE CASCADE 31 | ); 32 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Order") 13 | @Table( 14 | name = "\"order\"", 15 | schema = "public" 16 | ) 17 | public class OrderEntity { 18 | 19 | @Id 20 | @SequenceGenerator( 21 | name = "order_sequence", 22 | sequenceName = "order_sequence", 23 | allocationSize = 1 24 | ) 25 | @GeneratedValue( 26 | strategy = GenerationType.SEQUENCE, 27 | generator = "order_sequence" 28 | ) 29 | @Column( 30 | name = "id", 31 | updatable = false 32 | ) 33 | private Long id; 34 | 35 | @Column( 36 | name = "customer_id", 37 | nullable = false 38 | ) 39 | private Long customerId; 40 | 41 | @Column( 42 | name = "product_id", 43 | nullable = false 44 | ) 45 | private Long productId; 46 | 47 | @Column( 48 | name = "amount", 49 | nullable = false 50 | ) 51 | private double amount; 52 | 53 | @Column( 54 | name = "create_at", 55 | nullable = false, 56 | columnDefinition = "TIMESTAMP" 57 | ) 58 | private LocalDateTime createAt; 59 | } 60 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Payment") 13 | @Table(name = "payment") 14 | public class PaymentEntity { 15 | 16 | @Id 17 | @SequenceGenerator( 18 | name = "payment_sequence", 19 | sequenceName = "payment_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "payment_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Long id; 31 | 32 | @Column( 33 | name = "customer_id", 34 | nullable = false 35 | ) 36 | private Long customerId; 37 | 38 | @Column( 39 | name = "order_id", 40 | nullable = false 41 | ) 42 | private Long orderId; 43 | 44 | @Column( 45 | name = "create_at", 46 | nullable = false, 47 | columnDefinition = "TIMESTAMP" 48 | ) 49 | private LocalDateTime createAt; 50 | 51 | @Enumerated(EnumType.STRING) 52 | @Column(name = "status", nullable = false) 53 | @Builder.Default 54 | private PaymentStatus status = PaymentStatus.COMPLETED; 55 | } 56 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor @NoArgsConstructor 10 | @ToString @SuperBuilder 11 | @Entity(name = "Product") 12 | @Table(name = "product") 13 | public class ProductEntity { 14 | 15 | @Id 16 | @SequenceGenerator( 17 | name = "product_sequence", 18 | sequenceName = "product_sequence", 19 | allocationSize = 1 20 | ) 21 | @GeneratedValue( 22 | strategy = GenerationType.SEQUENCE, 23 | generator = "product_sequence" 24 | ) 25 | @Column( 26 | name = "id", 27 | updatable = false 28 | ) 29 | private Long id; 30 | 31 | @Column( 32 | name = "name", 33 | nullable = false, 34 | columnDefinition = "TEXT" 35 | ) 36 | private String name; 37 | 38 | @Column( 39 | name = "image", 40 | columnDefinition = "TEXT" 41 | ) 42 | private String image; 43 | 44 | @Column( 45 | name = "price", 46 | nullable = false, 47 | columnDefinition = "INTEGER" 48 | ) 49 | private Integer price; 50 | 51 | @Column( 52 | name = "available_quantity", 53 | nullable = false, 54 | columnDefinition = "INTEGER DEFAULT 0" 55 | ) 56 | private Integer availableQuantity; 57 | } 58 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/swagger/BaseController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.swagger; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class BaseController { 7 | public static final String CUSTOMER_TAG = "Customer Management"; 8 | public static final String CUSTOMER_DESCRIPTION = "Operations about customers including orders and payments"; 9 | public static final String PRODUCT_TAG = "Product Management"; 10 | public static final String PRODUCT_DESCRIPTION = "Operations for managing products catalog and inventory"; 11 | public static final String ORDER_TAG = "Order Management"; 12 | public static final String ORDER_DESCRIPTION = "Operations for managing customer orders and order processing"; 13 | public static final String PAYMENT_TAG = "Payment Management"; 14 | public static final String PAYMENT_DESCRIPTION = "Operations for managing payments and transactions"; 15 | public static final String NOTIFICATION_TAG = "Notification Management"; 16 | public static final String NOTIFICATION_DESCRIPTION = "Operations for managing notifications and communication with customers"; 17 | public static final String API_KEY_TAG = "API Key Management"; 18 | public static final String API_KEY_DESCRIPTION = "Operations for managing API keys and their authorizations"; 19 | public static final String APPLICATION_TAG = "Application Management"; 20 | public static final String APPLICATION_DESCRIPTION = "Operations for managing application access and permissions"; 21 | } 22 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationEntity; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | import jakarta.persistence.*; 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | 13 | @Entity 14 | @Table(name = "api_keys") 15 | @Data @SuperBuilder 16 | @NoArgsConstructor @AllArgsConstructor 17 | public class ApiKeyEntity { 18 | @Id 19 | @SequenceGenerator( 20 | name = "api_key_sequence", 21 | sequenceName = "api_key_sequence", 22 | allocationSize = 1 23 | ) 24 | @GeneratedValue( 25 | strategy = GenerationType.SEQUENCE, 26 | generator = "api_key_sequence" 27 | ) 28 | @Column( 29 | name = "id", 30 | updatable = false 31 | ) 32 | private Long id; 33 | 34 | @Column(unique = true, nullable = false) 35 | private String key; 36 | 37 | @Column(nullable = false, unique = true) 38 | private String client; 39 | 40 | private String description; 41 | 42 | private LocalDateTime createdDate; 43 | 44 | private LocalDateTime expirationDate; 45 | 46 | private boolean enabled; 47 | 48 | private boolean neverExpires; 49 | 50 | private boolean approved; 51 | 52 | private boolean revoked; 53 | 54 | @OneToMany(mappedBy = "apiKey") 55 | private List applications; 56 | } 57 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSEmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; 4 | import com.amazonaws.services.simpleemail.model.*; 5 | import dev.nano.notification.email.EmailService; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | @AllArgsConstructor 12 | @Profile("eks") 13 | public class AWSEmailService implements EmailService { 14 | 15 | private final AmazonSimpleEmailService emailService; 16 | 17 | public void send(String to, String content, String subject) { 18 | try { 19 | SendEmailRequest request = new SendEmailRequest() 20 | .withDestination(new Destination().withToAddresses(to)) 21 | .withMessage(new Message() 22 | .withBody(new Body() 23 | .withHtml(new Content() 24 | .withCharset(AWSConstant.UTF_ENCODING).withData(content))) 25 | .withSubject(new Content() 26 | .withCharset(AWSConstant.UTF_ENCODING).withData(subject))) 27 | .withSource(AWSConstant.SENDER); 28 | emailService.sendEmail(request); 29 | } catch (AmazonSimpleEmailServiceException e) { 30 | throw new IllegalStateException(AWSConstant.SEND_EMAIL_FAILED_EXCEPTION, e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor @NoArgsConstructor 10 | @ToString @SuperBuilder 11 | @Entity(name = "Customer") 12 | @Table( 13 | name = "customer", 14 | uniqueConstraints = { 15 | /* customize unique constraint */ 16 | @UniqueConstraint(name = "customer_email_unique", columnNames = "email") 17 | } 18 | ) 19 | public class CustomerEntity { 20 | 21 | @Id 22 | @SequenceGenerator( 23 | name = "customer_sequence", 24 | sequenceName = "customer_sequence", 25 | allocationSize = 1 26 | ) 27 | @GeneratedValue( 28 | strategy = GenerationType.SEQUENCE, 29 | generator = "customer_sequence" 30 | ) 31 | @Column( 32 | name = "id", 33 | updatable = false 34 | ) 35 | private Long id; 36 | 37 | @Column( 38 | name = "name", 39 | nullable = false, 40 | columnDefinition = "TEXT" 41 | ) 42 | private String name; 43 | 44 | @Column( 45 | name = "email", 46 | nullable = false 47 | ) 48 | private String email; 49 | 50 | @Column( 51 | name = "phone", 52 | columnDefinition = "TEXT" 53 | ) 54 | private String phone; 55 | 56 | @Column( 57 | name = "address", 58 | columnDefinition = "TEXT" 59 | ) 60 | private String address; 61 | } 62 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | import com.amazonaws.auth.AWSCredentials; 4 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 5 | import com.amazonaws.auth.BasicAWSCredentials; 6 | import com.amazonaws.regions.Regions; 7 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; 8 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.core.env.Environment; 13 | 14 | @Configuration 15 | @Profile("eks") 16 | public class AWSConfig { 17 | private final Environment environment; 18 | 19 | public AWSConfig(Environment environment) { 20 | this.environment = environment; 21 | } 22 | 23 | private static final String ACCESS_KEY_BUCKET = "ACCESS_KEY_BUCKET"; 24 | private static final String SECRET_KEY_BUCKET = "ACCESS_KEY_BUCKET"; 25 | 26 | @Bean 27 | AmazonSimpleEmailService emailService() { 28 | AWSCredentials awsCredentials = new BasicAWSCredentials( 29 | environment.getRequiredProperty(ACCESS_KEY_BUCKET), 30 | environment.getRequiredProperty(SECRET_KEY_BUCKET) 31 | ); 32 | return AmazonSimpleEmailServiceClientBuilder 33 | .standard() 34 | .withRegion(Regions.US_EAST_1) 35 | .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/keycloak/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: keycloak 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: keycloak 10 | template: 11 | metadata: 12 | labels: 13 | app: keycloak 14 | spec: 15 | containers: 16 | - name: keycloak 17 | image: quay.io/keycloak/keycloak:25.0.1 18 | args: ["start-dev", "--import-realm"] 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: KEYCLOAK_ADMIN 23 | value: "admin" 24 | - name: KEYCLOAK_ADMIN_PASSWORD 25 | value: "admin" 26 | - name: KC_DB 27 | value: "postgres" 28 | - name: KC_DB_URL_HOST 29 | value: "postgres" 30 | - name: KC_DB_URL_DATABASE 31 | value: "keycloak" 32 | - name: KC_DB_URL_PORT 33 | value: "5432" 34 | - name: KC_DB_USERNAME 35 | valueFrom: 36 | secretKeyRef: 37 | name: postgres-secret 38 | key: DB_USERNAME 39 | - name: KC_DB_PASSWORD 40 | valueFrom: 41 | secretKeyRef: 42 | name: postgres-secret 43 | key: DB_PASSWORD 44 | volumeMounts: 45 | - name: keycloak-realm-config 46 | mountPath: /opt/keycloak/data/import 47 | volumes: 48 | - name: keycloak-realm-config 49 | configMap: 50 | name: keycloak-realm-config 51 | restartPolicy: Always -------------------------------------------------------------------------------- /gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8765 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: gateway 9 | main: 10 | web-application-type: reactive 11 | allow-bean-definition-overriding: true 12 | security: 13 | api-key: 14 | enabled: false 15 | cloud: 16 | gateway: 17 | routes: 18 | - id: customer 19 | uri: lb://CUSTOMER # lb = load balancer 20 | predicates: 21 | - Path=/api/v1/customers/** 22 | - id: product 23 | uri: lb://PRODUCT 24 | predicates: 25 | - Path=/api/v1/products/** 26 | - id: order 27 | uri: lb://ORDER 28 | predicates: 29 | - Path=/api/v1/orders/** 30 | - id: payment 31 | uri: lb://PAYMENT 32 | predicates: 33 | - Path=/api/v1/payments/** 34 | - id: notification 35 | uri: lb://NOTIFICATION 36 | predicates: 37 | - Path=/api/v1/notifications/** 38 | - id: apikey-manager 39 | uri: lb://APIKEY-MANAGER 40 | predicates: 41 | - Path=/api/v1/apiKey-manager/** 42 | config: 43 | import: 44 | - "classpath:shared-application-${spring.profiles.active}.yml" 45 | - "classpath:clients-${spring.profiles.active}.properties" 46 | 47 | #Circuit Breaker 48 | resilience4j: 49 | circuitbreaker: 50 | instances: 51 | apiKeyAuthorization: 52 | slidingWindowSize: 10 53 | minimumNumberOfCalls: 5 54 | permittedNumberOfCallsInHalfOpenState: 3 55 | waitDurationInOpenState: 5s 56 | failureRateThreshold: 50 57 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment.config; 2 | 3 | import dev.nano.security.JwtAuthConverter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableMethodSecurity 18 | @RequiredArgsConstructor 19 | public class SecurityConfig { 20 | 21 | private final JwtAuthConverter jwtAuthConverter; 22 | 23 | @Bean 24 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 25 | http 26 | .csrf(AbstractHttpConfigurer::disable) 27 | .authorizeHttpRequests(authorize -> authorize 28 | .anyRequest().authenticated() 29 | ) 30 | .oauth2ResourceServer(oauth2 -> oauth2 31 | .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) 32 | ) 33 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); 34 | return http.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product.config; 2 | 3 | import dev.nano.security.JwtAuthConverter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableMethodSecurity 18 | @RequiredArgsConstructor 19 | public class SecurityConfig { 20 | 21 | private final JwtAuthConverter jwtAuthConverter; 22 | 23 | @Bean 24 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 25 | http 26 | .csrf(AbstractHttpConfigurer::disable) 27 | .authorizeHttpRequests(authorize -> authorize 28 | .anyRequest().authenticated() 29 | ) 30 | .oauth2ResourceServer(oauth2 -> oauth2 31 | .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) 32 | ) 33 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); 34 | return http.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer.config; 2 | 3 | import dev.nano.security.JwtAuthConverter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableMethodSecurity 18 | @RequiredArgsConstructor 19 | public class SecurityConfig { 20 | 21 | private final JwtAuthConverter jwtAuthConverter; 22 | 23 | @Bean 24 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 25 | http 26 | .csrf(AbstractHttpConfigurer::disable) 27 | .authorizeHttpRequests(authorize -> authorize 28 | .anyRequest().authenticated() 29 | ) 30 | .oauth2ResourceServer(oauth2 -> oauth2 31 | .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) 32 | ) 33 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); 34 | return http.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.config; 2 | 3 | import dev.nano.security.JwtAuthConverter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableMethodSecurity 18 | @RequiredArgsConstructor 19 | public class SecurityConfig { 20 | 21 | private final JwtAuthConverter jwtAuthConverter; 22 | 23 | @Bean 24 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 25 | http 26 | .csrf(AbstractHttpConfigurer::disable) 27 | .authorizeHttpRequests(authorize -> authorize 28 | .anyRequest().authenticated() 29 | ) 30 | .oauth2ResourceServer(oauth2 -> oauth2 31 | .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) 32 | ) 33 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); 34 | return http.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/swagger/OpenAPIConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.swagger; 2 | 3 | import io.swagger.v3.oas.models.ExternalDocumentation; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Contact; 6 | import io.swagger.v3.oas.models.info.Info; 7 | import io.swagger.v3.oas.models.info.License; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | @EnableConfigurationProperties(OpenAPIProperties.class) 14 | public class OpenAPIConfig { 15 | 16 | private final OpenAPIProperties properties; 17 | 18 | public OpenAPIConfig(OpenAPIProperties properties) { 19 | this.properties = properties; 20 | } 21 | 22 | @Bean 23 | public OpenAPI customOpenAPI() { 24 | return new OpenAPI() 25 | .info(new Info() 26 | .title(properties.getTitle()) 27 | .version(properties.getVersion()) 28 | .description(properties.getDescription()) 29 | .contact(new Contact() 30 | .name(properties.getContact().getName()) 31 | .email(properties.getContact().getEmail()) 32 | .url(properties.getContact().getUrl())) 33 | .license(new License() 34 | .name(properties.getLicense().getName()) 35 | .url(properties.getLicense().getUrl()))) 36 | .externalDocs(new ExternalDocumentation() 37 | .description(properties.getExternalDocs().getDescription()) 38 | .url(properties.getExternalDocs().getUrl())); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order.config; 2 | 3 | import dev.nano.security.JwtAuthConverter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 13 | 14 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 15 | 16 | @Configuration 17 | @EnableWebSecurity 18 | @EnableMethodSecurity 19 | @RequiredArgsConstructor 20 | public class SecurityConfig { 21 | 22 | @Bean 23 | public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthConverter jwtAuthConverter, ApiKeyAuthFilter apiKeyAuthFilter) throws Exception { 24 | http 25 | .csrf(AbstractHttpConfigurer::disable) 26 | .authorizeHttpRequests(authorize -> authorize 27 | .anyRequest().authenticated() 28 | ) 29 | .oauth2ResourceServer(oauth2 -> oauth2 30 | .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) 31 | ) 32 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) 33 | .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class); 34 | return http.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Notification") 13 | @Table(name = "notification") 14 | public class NotificationEntity { 15 | 16 | @Id 17 | @SequenceGenerator( 18 | name = "notification_sequence", 19 | sequenceName = "notification_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "notification_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Long id; 31 | 32 | @Column( 33 | name = "customer_id", 34 | nullable = false 35 | ) 36 | private Long customerId; 37 | 38 | @Column( 39 | name = "customer_name", 40 | columnDefinition = "TEXT" 41 | ) 42 | private String customerName; 43 | 44 | @Column( 45 | name = "customer_email", 46 | columnDefinition = "TEXT" 47 | ) 48 | private String customerEmail; 49 | 50 | @Column( 51 | name = "sender", 52 | nullable = false, 53 | columnDefinition = "TEXT" 54 | ) 55 | private String sender; 56 | 57 | @Column( 58 | name = "message", 59 | nullable = false, 60 | columnDefinition = "TEXT" 61 | ) 62 | private String message; 63 | 64 | @Column( 65 | name = "sent_at", 66 | nullable = false, 67 | columnDefinition = "TIMESTAMP" 68 | ) 69 | private LocalDateTime sentAt; 70 | } 71 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: rabbitmq-config 5 | data: 6 | enabled_plugins: | 7 | [rabbitmq_management,rabbitmq_peer_discovery_k8s]. 8 | 9 | rabbitmq.conf: | 10 | ## Cluster formation. See https://www.rabbitmq.com/cluster-formation.html to learn more. 11 | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s 12 | cluster_formation.k8s.host = kubernetes.default.svc.cluster.local 13 | ## Should RabbitMQ node name be computed from the pod's hostname or IP address? 14 | ## IP addresses are not stable, so using [stable] hostnames is recommended when possible. 15 | ## Set to "hostname" to use pod hostnames. 16 | ## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME 17 | ## environment variable. 18 | cluster_formation.k8s.address_type = hostname 19 | ## How often should node cleanup checks run? 20 | cluster_formation.node_cleanup.interval = 30 21 | ## Set to false if automatic removal of unknown/absent nodes 22 | ## is desired. This can be dangerous, see 23 | ## * https://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup 24 | ## * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ 25 | cluster_formation.node_cleanup.only_log_warning = true 26 | cluster_partition_handling = autoheal 27 | ## See https://www.rabbitmq.com/ha.html#master-migration-data-locality 28 | queue_master_locator=min-masters 29 | ## This is just an example. 30 | ## This enables remote access for the default user with well known credentials. 31 | ## Consider deleting the default user and creating a separate user with a set of generated 32 | ## credentials instead. 33 | ## Learn more at https://www.rabbitmq.com/access-control.html#loopback-users 34 | loopback_users.guest = false 35 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: rabbitmq-config 5 | data: 6 | enabled_plugins: | 7 | [rabbitmq_management,rabbitmq_peer_discovery_k8s]. 8 | 9 | rabbitmq.conf: | 10 | ## Cluster formation. See https://www.rabbitmq.com/cluster-formation.html to learn more. 11 | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s 12 | cluster_formation.k8s.host = kubernetes.default.svc.cluster.local 13 | ## Should RabbitMQ node name be computed from the pod's hostname or IP address? 14 | ## IP addresses are not stable, so using [stable] hostnames is recommended when possible. 15 | ## Set to "hostname" to use pod hostnames. 16 | ## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME 17 | ## environment variable. 18 | cluster_formation.k8s.address_type = hostname 19 | ## How often should node cleanup checks run? 20 | cluster_formation.node_cleanup.interval = 30 21 | ## Set to false if automatic removal of unknown/absent nodes 22 | ## is desired. This can be dangerous, see 23 | ## * https://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup 24 | ## * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ 25 | cluster_formation.node_cleanup.only_log_warning = true 26 | cluster_partition_handling = autoheal 27 | ## See https://www.rabbitmq.com/ha.html#master-migration-data-locality 28 | queue_master_locator=min-masters 29 | ## This is just an example. 30 | ## This enables remote access for the default user with well known credentials. 31 | ## Consider deleting the default user and creating a separate user with a set of generated 32 | ## credentials instead. 33 | ## Learn more at https://www.rabbitmq.com/access-control.html#loopback-users 34 | loopback_users.guest = false 35 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/ingress/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: main-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: /$2 7 | spec: 8 | rules: 9 | - http: 10 | paths: 11 | - path: /api/v1/customer(/|$)(.*) 12 | pathType: ImplementationSpecific 13 | backend: 14 | service: 15 | name: customer 16 | port: 17 | number: 8001 18 | - path: /api/v1/product(/|$)(.*) 19 | pathType: ImplementationSpecific 20 | backend: 21 | service: 22 | name: product 23 | port: 24 | number: 8002 25 | - path: /api/v1/order(/|$)(.*) 26 | pathType: ImplementationSpecific 27 | backend: 28 | service: 29 | name: order 30 | port: 31 | number: 8003 32 | - path: /api/v1/notification(/|$)(.*) 33 | pathType: ImplementationSpecific 34 | backend: 35 | service: 36 | name: notification 37 | port: 38 | number: 8004 39 | - path: /api/v1/payment(/|$)(.*) 40 | pathType: ImplementationSpecific 41 | backend: 42 | service: 43 | name: payment 44 | port: 45 | number: 8005 46 | - path: /api/v1/apiKey-manager(/|$)(.*) 47 | pathType: ImplementationSpecific 48 | backend: 49 | service: 50 | name: apikey-manager 51 | port: 52 | number: 8006 53 | - path: /realms(/|$)(.*) 54 | pathType: ImplementationSpecific 55 | backend: 56 | service: 57 | name: keycloak 58 | port: 59 | number: 8080 60 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-docker.yml: -------------------------------------------------------------------------------- 1 | management: 2 | observations: 3 | key-values: 4 | application: ${spring.application.name} 5 | tracing: 6 | sampling: 7 | probability: 1.0 8 | enabled: true 9 | zipkin: 10 | tracing: 11 | endpoint: http://localhost:9411/api/v2/spans 12 | endpoints: 13 | web: 14 | exposure: 15 | include: "*" 16 | endpoint: 17 | health: 18 | show-details: always 19 | 20 | spring: 21 | datasource: 22 | url: jdbc:postgresql://postgres:5432/${spring.application.name} 23 | username: miliariadnane 24 | password: password 25 | jpa: 26 | hibernate: 27 | ddl-auto: none 28 | rabbitmq: 29 | addresses: rabbitmq:5672 30 | sql: 31 | init: 32 | mode: always 33 | schema-locations: classpath*:db/schema.sql 34 | data-locations: classpath*:db/data.sql 35 | platform: postgresql 36 | security: 37 | oauth2: 38 | resourceserver: 39 | jwt: 40 | issuer-uri: http://keycloak:8080/realms/demo-realm 41 | 42 | eureka: 43 | client: 44 | service-url: 45 | defaultZone: http://eureka-server:8761/eureka 46 | 47 | jwt: 48 | auth: 49 | converter: 50 | principle-attribute: preferred_username 51 | 52 | resilience4j: 53 | circuitbreaker: 54 | instances: 55 | productService: 56 | sliding-window-type: count_based 57 | sliding-window-size: 5 58 | minimum-number-of-calls: 5 59 | failure-rate-threshold: 50 60 | wait-duration-in-open-state: 30s 61 | orderService: 62 | sliding-window-type: count_based 63 | sliding-window-size: 5 64 | minimum-number-of-calls: 5 65 | failure-rate-threshold: 50 66 | wait-duration-in-open-state: 30s 67 | retry: 68 | instances: 69 | orderService: 70 | max-attempts: 3 71 | wait-duration: 1000ms 72 | timelimiter: 73 | instances: 74 | orderService: 75 | timeout-duration: 3s 76 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import dev.nano.apikey.ApiKeyEntity; 4 | import dev.nano.apikey.ApiKeyRepository; 5 | import dev.nano.exceptionhandler.core.ResourceNotFoundException; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | import static dev.nano.apikey.ApiKeyConstant.API_KEY_NOT_FOUND; 10 | import static dev.nano.application.ApplicationConstant.APPLICATION_NOT_FOUND; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class ApplicationServiceImpl implements ApplicationService{ 15 | private final ApplicationRepository applicationRepository; 16 | private final ApiKeyRepository apiKeyRepository; 17 | 18 | @Override 19 | public void assignApplicationToApiKey(ApplicationName applicationName, String apiKey) { 20 | ApiKeyEntity key = apiKeyRepository.findByKey(apiKey) 21 | .orElseThrow(() -> new ResourceNotFoundException(API_KEY_NOT_FOUND)); 22 | 23 | ApplicationEntity application = ApplicationEntity.builder() 24 | .applicationName(applicationName) 25 | .apiKey(key) 26 | .revoked(false) 27 | .enabled(true) 28 | .approved(true) 29 | .build(); 30 | 31 | applicationRepository.save(application); 32 | } 33 | 34 | @Override 35 | public void revokeApplicationFromApiKey(String applicationName, String apiKey) { 36 | if(!apiKeyRepository.doesKeyExists(apiKey)) 37 | throw new ResourceNotFoundException(API_KEY_NOT_FOUND); 38 | 39 | ApplicationEntity application = applicationRepository.findByApplicationName(applicationName) 40 | .orElseThrow(() -> new ResourceNotFoundException(APPLICATION_NOT_FOUND)); 41 | 42 | application.setRevoked(true); 43 | application.setEnabled(false); 44 | application.setApproved(false); 45 | 46 | applicationRepository.save(application); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docker/config/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 8 | rule_files: 9 | # - "first_rules.yml" 10 | # - "second_rules.yml" 11 | 12 | # A scrape configuration containing exactly one endpoint to scrape: 13 | # Here it's Prometheus itself. 14 | scrape_configs: 15 | # The job name is added as a label `job=` to any timeseries scraped from this config. 16 | - job_name: 'prometheus' 17 | # metrics_path defaults to '/metrics' 18 | # scheme defaults to 'http'. 19 | static_configs: 20 | - targets: ['localhost:9090'] 21 | 22 | - job_name: 'customer' 23 | metrics_path: '/actuator/prometheus' 24 | scrape_interval: 5s 25 | static_configs: 26 | - targets: ['customer:8001'] 27 | 28 | - job_name: 'product' 29 | metrics_path: '/actuator/prometheus' 30 | scrape_interval: 5s 31 | static_configs: 32 | - targets: ['product:8002'] 33 | 34 | - job_name: 'order' 35 | metrics_path: '/actuator/prometheus' 36 | scrape_interval: 5s 37 | static_configs: 38 | - targets: ['order:8003'] 39 | 40 | - job_name: 'notification' 41 | metrics_path: '/actuator/prometheus' 42 | scrape_interval: 5s 43 | static_configs: 44 | - targets: ['notification:8004'] 45 | 46 | - job_name: 'payment' 47 | metrics_path: '/actuator/prometheus' 48 | scrape_interval: 5s 49 | static_configs: 50 | - targets: ['payment:8005'] 51 | 52 | - job_name: 'eureka-server' 53 | metrics_path: '/actuator/prometheus' 54 | scrape_interval: 5s 55 | static_configs: 56 | - targets: ['eureka-server:8761'] 57 | 58 | - job_name: 'gateway' 59 | metrics_path: '/actuator/prometheus' 60 | scrape_interval: 5s 61 | static_configs: 62 | - targets: ['gateway:8765'] 63 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.security.authentication.AbstractAuthenticationToken; 7 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 8 | import org.springframework.security.config.web.server.ServerHttpSecurity; 9 | import org.springframework.security.oauth2.jwt.Jwt; 10 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; 11 | import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; 12 | import org.springframework.security.web.server.SecurityWebFilterChain; 13 | import reactor.core.publisher.Mono; 14 | 15 | @Configuration 16 | @EnableWebFluxSecurity 17 | public class SecurityConfig { 18 | 19 | @Bean 20 | public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { 21 | http 22 | .csrf(ServerHttpSecurity.CsrfSpec::disable) 23 | .authorizeExchange(exchange -> exchange 24 | .pathMatchers("/eureka/**").permitAll() 25 | .anyExchange().authenticated() 26 | ) 27 | .oauth2ResourceServer(oauth2 -> oauth2 28 | .jwt(jwt -> jwt 29 | .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) 30 | ) 31 | ); 32 | return http.build(); 33 | } 34 | 35 | private Converter> grantedAuthoritiesExtractor() { 36 | JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); 37 | jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter()); 38 | return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); 39 | } 40 | } -------------------------------------------------------------------------------- /docker/compose/docker-compose-local.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | container_name: postgres-local 4 | image: postgres:15.5-alpine 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: password 8 | POSTGRES_DB: keycloak 9 | PGDATA: /data/postgres 10 | volumes: 11 | - postgres-local:/data/postgres 12 | ports: 13 | - "5433:5432" 14 | networks: 15 | - postgres-local 16 | restart: unless-stopped 17 | 18 | pgadmin: 19 | container_name: pgadmin-local 20 | image: dpage/pgadmin4 21 | environment: 22 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 23 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} 24 | PGADMIN_CONFIG_SERVER_MODE: 'False' 25 | volumes: 26 | - pgadmin-local:/var/lib/pgadmin 27 | ports: 28 | - "5050:80" 29 | networks: 30 | - postgres-local 31 | restart: unless-stopped 32 | 33 | zipkin: 34 | image: openzipkin/zipkin:latest 35 | container_name: zipkin-local 36 | ports: 37 | - "9411:9411" 38 | networks: 39 | - spring-local 40 | 41 | rabbitmq: 42 | image: rabbitmq:3.12-management-alpine 43 | container_name: rabbitmq-local 44 | ports: 45 | - "5672:5672" 46 | - "15672:15672" 47 | networks: 48 | - spring-local 49 | 50 | keycloak: 51 | container_name: keycloak-local 52 | image: quay.io/keycloak/keycloak:25.0.1 53 | command: start-dev --import-realm 54 | volumes: 55 | - ../config/keycloak:/opt/keycloak/data/import 56 | environment: 57 | KEYCLOAK_ADMIN: admin 58 | KEYCLOAK_ADMIN_PASSWORD: admin 59 | KC_DB: postgres 60 | KC_DB_URL_HOST: postgres-local 61 | KC_DB_URL_DATABASE: keycloak 62 | KC_DB_URL_PORT: 5432 63 | KC_DB_USERNAME: postgres 64 | KC_DB_PASSWORD: password 65 | ports: 66 | - "8180:8080" 67 | networks: 68 | - spring-local 69 | - postgres-local 70 | depends_on: 71 | - postgres 72 | 73 | networks: 74 | postgres-local: 75 | driver: bridge 76 | spring-local: 77 | driver: bridge 78 | 79 | volumes: 80 | postgres-local: 81 | pgadmin-local: 82 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v4beta1 2 | kind: Config 3 | metadata: 4 | name: demo-microservices 5 | build: 6 | local: 7 | push: false 8 | artifacts: 9 | - image: miliariadnane/customer 10 | jib: 11 | type: maven 12 | project: customer 13 | args: 14 | - "-Dspring.profiles.active=kube" 15 | - image: miliariadnane/product 16 | jib: 17 | type: maven 18 | project: product 19 | args: 20 | - "-Dspring.profiles.active=kube" 21 | - image: miliariadnane/order 22 | jib: 23 | type: maven 24 | project: order 25 | args: 26 | - "-Dspring.profiles.active=kube" 27 | - image: miliariadnane/notification 28 | jib: 29 | type: maven 30 | project: notification 31 | args: 32 | - "-Dspring.profiles.active=kube" 33 | - image: miliariadnane/payment 34 | jib: 35 | type: maven 36 | project: payment 37 | args: 38 | - "-Dspring.profiles.active=kube" 39 | - image: miliariadnane/apikey-manager 40 | jib: 41 | type: maven 42 | project: apikey-manager 43 | args: 44 | - "-Dspring.profiles.active=kube" 45 | manifests: 46 | rawYaml: 47 | - k8s/minikube/bootstrap/postgres/*.yml 48 | - k8s/minikube/bootstrap/rabbitmq/*.yml 49 | - k8s/minikube/bootstrap/zipkin/*.yml 50 | - k8s/minikube/bootstrap/keycloak/*.yml 51 | - k8s/minikube/bootstrap/prometheus/*.yml 52 | - k8s/minikube/bootstrap/grafana/*.yml 53 | - k8s/minikube/bootstrap/ingress/*.yml 54 | - k8s/minikube/services/customer/*.yml 55 | - k8s/minikube/services/product/*.yml 56 | - k8s/minikube/services/order/*.yml 57 | - k8s/minikube/services/notification/*.yml 58 | - k8s/minikube/services/payment/*.yml 59 | - k8s/minikube/services/apikey-manager/*.yml 60 | deploy: 61 | kubectl: {} 62 | portForward: 63 | - resourceType: service 64 | resourceName: keycloak 65 | port: 8080 66 | localPort: 8180 67 | - resourceType: service 68 | resourceName: prometheus 69 | port: 9090 70 | localPort: 9090 71 | - resourceType: service 72 | resourceName: grafana 73 | port: 3000 74 | localPort: 3000 -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-default.yml: -------------------------------------------------------------------------------- 1 | management: 2 | observations: 3 | key-values: 4 | application: ${spring.application.name} 5 | tracing: 6 | sampling: 7 | probability: 1.0 8 | enabled: true 9 | zipkin: 10 | tracing: 11 | endpoint: http://localhost:9411/api/v2/spans 12 | endpoints: 13 | web: 14 | exposure: 15 | include: "*" 16 | endpoint: 17 | health: 18 | show-details: always 19 | 20 | spring: 21 | datasource: 22 | url: jdbc:postgresql://localhost:5433/${spring.application.name} 23 | username: postgres 24 | password: password 25 | jpa: 26 | hibernate: 27 | ddl-auto: create-drop 28 | properties: 29 | hibernate: 30 | dialect: org.hibernate.dialect.PostgreSQLDialect 31 | format_sql: true 32 | show-sql: true 33 | open-in-view: false 34 | defer-datasource-initialization: true 35 | sql: 36 | init: 37 | mode: always 38 | data-locations: classpath*:db/data.sql 39 | platform: postgresql 40 | rabbitmq: 41 | addresses: localhost:5672 42 | security: 43 | oauth2: 44 | resourceserver: 45 | jwt: 46 | issuer-uri: http://localhost:8180/realms/demo-realm 47 | 48 | eureka: 49 | client: 50 | service-url: 51 | defaultZone: http://localhost:8761/eureka 52 | fetch-registry: true 53 | register-with-eureka: true 54 | 55 | logging: 56 | pattern: 57 | correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" 58 | level: "%5p ${logging.pattern.correlation} %c{1.} : %m%n" 59 | 60 | jwt: 61 | auth: 62 | converter: 63 | principle-attribute: preferred_username 64 | 65 | resilience4j: 66 | circuitbreaker: 67 | instances: 68 | productService: 69 | sliding-window-type: count_based 70 | sliding-window-size: 5 71 | minimum-number-of-calls: 5 72 | failure-rate-threshold: 50 73 | wait-duration-in-open-state: 30s 74 | orderService: 75 | sliding-window-type: count_based 76 | sliding-window-size: 5 77 | minimum-number-of-calls: 5 78 | failure-rate-threshold: 50 79 | wait-duration-in-open-state: 30s 80 | retry: 81 | instances: 82 | orderService: 83 | max-attempts: 3 84 | wait-duration: 1000ms 85 | timelimiter: 86 | instances: 87 | orderService: 88 | timeout-duration: 3s 89 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiAuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 7 | import org.springframework.cloud.gateway.filter.GlobalFilter; 8 | import org.springframework.cloud.gateway.route.Route; 9 | import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; 10 | import org.springframework.context.annotation.Lazy; 11 | import org.springframework.core.Ordered; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.server.ResponseStatusException; 15 | import org.springframework.web.server.ServerWebExchange; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.util.List; 19 | import java.util.Objects; 20 | 21 | @Component 22 | public class ApiAuthorizationFilter implements GlobalFilter, Ordered { 23 | 24 | @Value("${spring.security.api-key.enabled:true}") 25 | private boolean apiKeyEnabled; 26 | 27 | final ApiKeyAuthorizationChecker apiKeyAuthorizationChecker; 28 | 29 | @Autowired 30 | public ApiAuthorizationFilter( 31 | @Qualifier("main-checker") @Lazy ApiKeyAuthorizationChecker apiKeyAuthorizationChecker 32 | ) { 33 | this.apiKeyAuthorizationChecker = apiKeyAuthorizationChecker; 34 | } 35 | 36 | @Override 37 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 38 | if (!apiKeyEnabled) { 39 | return chain.filter(exchange); 40 | } 41 | 42 | Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); 43 | String applicationName = route.getId(); 44 | List apiKey = exchange.getRequest().getHeaders().get("ApiKey"); 45 | 46 | if (applicationName == null || Objects.requireNonNull(apiKey).isEmpty()) { 47 | throw new ResponseStatusException( 48 | HttpStatus.UNAUTHORIZED, 49 | "Application name is not defined, you are not authorized to access this resource" 50 | ); 51 | } 52 | 53 | return chain.filter(exchange); 54 | } 55 | 56 | @Override 57 | public int getOrder() { 58 | return Ordered.LOWEST_PRECEDENCE; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | -- First insert API Keys and store their IDs 2 | INSERT INTO api_keys (id, key, client, description, created_date, expiration_date, enabled, never_expires, approved, 3 | revoked) 4 | SELECT nextval('api_key_sequence'), t.key, t.client, t.description, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '365 days', t.enabled, t.never_expires, t.approved, t.revoked 5 | FROM ( 6 | VALUES 7 | ('ecom-frontend-key-2024', 'E-commerce Frontend', 'API Key for frontend application', true, false, true, false), 8 | ('mobile-app-key-2024', 'Mobile Application', 'API Key for mobile app', true, false, true, false), 9 | ('admin-dashboard-key-2024', 'Admin Dashboard', 'API Key for admin dashboard', true, true, true, false), 10 | -- Internal traffic key (never exposed publicly) 11 | ('internal-service-key', 'Internal Service', 'API Key for inter-service calls', true, true, true, false) 12 | ) AS t(key, client, description, enabled, never_expires, approved, revoked); 13 | 14 | -- Then insert Applications using the actual API key IDs by looking up the key value 15 | INSERT INTO applications (id, application_name, enabled, approved, revoked, api_key_id) 16 | SELECT nextval('application_sequence'), 17 | t.app_name, 18 | true, 19 | true, 20 | false, 21 | (SELECT ak.id FROM api_keys ak WHERE ak.key = t.api_key_name) 22 | FROM (VALUES ('CUSTOMER', 'ecom-frontend-key-2024'), 23 | ('CUSTOMER', 'mobile-app-key-2024'), 24 | ('CUSTOMER', 'admin-dashboard-key-2024'), 25 | ('PRODUCT', 'ecom-frontend-key-2024'), 26 | ('PRODUCT', 'mobile-app-key-2024'), 27 | ('PRODUCT', 'admin-dashboard-key-2024'), 28 | ('ORDER', 'ecom-frontend-key-2024'), 29 | ('ORDER', 'mobile-app-key-2024'), 30 | ('ORDER', 'admin-dashboard-key-2024'), 31 | ('PAYMENT', 'ecom-frontend-key-2024'), 32 | ('PAYMENT', 'admin-dashboard-key-2024'), 33 | ('NOTIFICATION', 'admin-dashboard-key-2024'), 34 | -- Grant the internal-service-key to every microservice that consumes internal APIs 35 | ('CUSTOMER', 'internal-service-key'), 36 | ('PRODUCT', 'internal-service-key'), 37 | ('ORDER', 'internal-service-key'), -- This line specifically authorizes 'internal-service-key' for the 'ORDER' application 38 | ('PAYMENT', 'internal-service-key'), 39 | ('NOTIFICATION', 'internal-service-key'), 40 | ('APIKEY_MANAGER', 'internal-service-key') 41 | ) AS t(app_name, api_key_name); 42 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentProcessor.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import dev.nano.clients.order.OrderClient; 4 | import dev.nano.clients.notification.NotificationRequest; 5 | import dev.nano.amqp.RabbitMQProducer; 6 | import dev.nano.clients.customer.CustomerClient; 7 | import dev.nano.clients.customer.CustomerResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | import io.github.resilience4j.retry.annotation.Retry; 13 | 14 | import java.time.LocalDateTime; 15 | import java.util.List; 16 | 17 | @Component 18 | @RequiredArgsConstructor 19 | @Slf4j 20 | public class PaymentProcessor { 21 | 22 | private final PaymentRepository paymentRepository; 23 | private final OrderClient orderClient; 24 | private final CustomerClient customerClient; 25 | private final RabbitMQProducer rabbitMQProducer; 26 | 27 | @Scheduled(fixedDelayString = "60000") 28 | @Retry(name = "orderService") 29 | public void processPendingPayments() { 30 | List pendingPayments = paymentRepository.findAllByStatus(PaymentStatus.PENDING); 31 | if (pendingPayments.isEmpty()) { 32 | return; 33 | } 34 | 35 | pendingPayments.forEach(payment -> { 36 | try { 37 | orderClient.getOrder(payment.getOrderId()); 38 | CustomerResponse customer = customerClient.getCustomer(payment.getCustomerId()); 39 | payment.setStatus(PaymentStatus.COMPLETED); 40 | paymentRepository.save(payment); 41 | 42 | NotificationRequest notificationRequest = NotificationRequest.builder() 43 | .customerId(customer.getId()) 44 | .customerName(customer.getName()) 45 | .customerEmail(customer.getEmail()) 46 | .sender("nanodev") 47 | .message("Your pending payment has been processed successfully at " + LocalDateTime.now()) 48 | .build(); 49 | 50 | rabbitMQProducer.publish( 51 | "internal.exchange", 52 | "internal.notification.routing-key", 53 | notificationRequest 54 | ); 55 | log.info("Successfully processed pending payment with id {}", payment.getId()); 56 | } catch (Exception ex) { 57 | log.error("Failed to process pending payment with id {}: {}", payment.getId(), ex.getMessage()); 58 | } 59 | }); 60 | } 61 | } -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiKeyManagerAuthorizationChecker.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import dev.nano.clients.apiKeyManager.apiKey.ApiKeyManagerClient; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * Service responsible for checking API key authorization against the apiKey-manager service. 12 | * 13 | * Features: 14 | * - Caches authorization results to improve performance 15 | * - Uses circuit breaker pattern for resilience 16 | * - Integrates with apiKey-manager service via Feign client 17 | * - Provides fallback mechanism for service unavailability 18 | * 19 | * Authorization Flow: 20 | * 1. Check cache for existing authorization decision 21 | * 2. If not in cache, call apiKey-manager service 22 | * 3. Cache the result for future requests 23 | * 4. Handle service failures with circuit breaker 24 | */ 25 | 26 | @Service("main-checker") 27 | @AllArgsConstructor 28 | @Slf4j 29 | public class ApiKeyManagerAuthorizationChecker implements ApiKeyAuthorizationChecker { 30 | 31 | private final ApiKeyManagerClient apiKeyManagerClient; 32 | 33 | /** 34 | * Checks if an API key is authorized for a specific application 35 | * 36 | * @param apiKey the API key to validate 37 | * @param applicationName the target application/service name 38 | * @return true if authorized, false otherwise 39 | */ 40 | 41 | @Override 42 | @Cacheable(value = "apikey-authorizations", key = "#apiKey + '-' + #applicationName") 43 | @CircuitBreaker(name = "apiKeyAuthorization", fallbackMethod = "fallbackIsAuthorized") 44 | public boolean isAuthorized(String apiKey, String applicationName) { 45 | return apiKeyManagerClient.isKeyAuthorizedForApplication( 46 | apiKey, 47 | applicationName 48 | ); 49 | } 50 | 51 | /** 52 | * Fallback method when apiKey-manager service is unavailable 53 | * Returns false to maintain security in case of service failure 54 | * 55 | * @param apiKey the API key that was being checked 56 | * @param applicationName the target application 57 | * @param ex the exception that triggered the fallback 58 | * @return false to deny access when service is unavailable 59 | */ 60 | private boolean fallbackIsAuthorized(String apiKey, String applicationName, Exception ex) { 61 | log.error("Failed to check API key authorization for key: {} and application: {}. Error: {}", 62 | apiKey, applicationName, ex.getMessage()); 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/controller/ApplicationController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.controller; 2 | 3 | import dev.nano.application.ApplicationConstant; 4 | import dev.nano.application.ApplicationName; 5 | import dev.nano.application.ApplicationService; 6 | import io.swagger.v3.oas.annotations.Operation; 7 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.web.bind.annotation.DeleteMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import dev.nano.swagger.BaseController; 17 | 18 | @RestController 19 | @RequestMapping(path = ApplicationConstant.APPLICATION_URI_REST_API) 20 | @Tag(name = BaseController.APPLICATION_TAG, description = BaseController.APPLICATION_DESCRIPTION) 21 | @AllArgsConstructor 22 | public class ApplicationController { 23 | private final ApplicationService applicationService; 24 | 25 | @Operation( 26 | summary = "Revoke application access", 27 | description = "Remove application access from a specific API key" 28 | ) 29 | @ApiResponses(value = { 30 | @ApiResponse(responseCode = "200", description = "Application access revoked successfully"), 31 | @ApiResponse(responseCode = "404", description = "Application or API key not found"), 32 | @ApiResponse(responseCode = "500", description = "Internal server error") 33 | }) 34 | @DeleteMapping("{applicationName}/revoke/{apiKey}") 35 | public void revokeApplicationFromApiKey( 36 | @PathVariable("applicationName") String applicationName, 37 | @PathVariable("apiKey") String apiKey) { 38 | applicationService.revokeApplicationFromApiKey(applicationName, apiKey); 39 | } 40 | 41 | @Operation( 42 | summary = "Assign application to API key", 43 | description = "Grant application access to a specific API key" 44 | ) 45 | @ApiResponses(value = { 46 | @ApiResponse(responseCode = "200", description = "Application assigned successfully"), 47 | @ApiResponse(responseCode = "404", description = "API key not found"), 48 | @ApiResponse(responseCode = "500", description = "Internal server error") 49 | }) 50 | @PutMapping("{applicationName}/revoke/{apiKey}") 51 | public void assignApplicationToApiKey( 52 | @PathVariable("applicationName") ApplicationName applicationName, 53 | @PathVariable("apiKey") String apiKey) { 54 | applicationService.assignApplicationToApiKey(applicationName, apiKey); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /amqp/src/main/java/dev/nano/amqp/RabbitMQConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.amqp; 2 | 3 | import org.springframework.amqp.core.*; 4 | import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; 5 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 6 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 7 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 8 | import org.springframework.amqp.support.converter.MessageConverter; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Primary; 13 | 14 | @Configuration 15 | public class RabbitMQConfig { 16 | 17 | private ConnectionFactory connectionFactory; 18 | @Value("${rabbitmq.exchange.internal}") 19 | private String internalExchange; 20 | @Value("${rabbitmq.queue.notification}") 21 | private String notificationQueue; 22 | @Value("${rabbitmq.routing-key.internal-notification}") 23 | private String internalNotificationRoutingKey; 24 | 25 | public RabbitMQConfig(ConnectionFactory connectionFactory) { 26 | this.connectionFactory = connectionFactory; 27 | } 28 | 29 | @Bean 30 | Queue notificationQueue() { 31 | return new Queue(this.notificationQueue); 32 | } 33 | 34 | @Bean 35 | TopicExchange internalExchange() { 36 | return new TopicExchange(this.internalExchange); 37 | } 38 | 39 | // bind queue to exchange with routing key 40 | @Bean 41 | Binding binding() { 42 | return BindingBuilder 43 | .bind(notificationQueue()) 44 | .to(internalExchange()) 45 | .with(this.internalNotificationRoutingKey); 46 | } 47 | 48 | // Configure amqp template that allows to send messages 49 | @Primary 50 | @Bean 51 | AmqpTemplate amqpTemplate() { 52 | RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); 53 | rabbitTemplate.setMessageConverter(jacksonConverter()); 54 | return rabbitTemplate; 55 | } 56 | 57 | //Build the rabbit listener container & connect to RabbitMQ broker to listener message 58 | // that allows to consume messages from queues (listener) 59 | @Bean 60 | SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory() { 61 | SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); 62 | factory.setConnectionFactory(connectionFactory); 63 | factory.setMessageConverter(jacksonConverter()); 64 | return factory; 65 | } 66 | 67 | // Jackson2JsonMessageConverter is used to convert messages to JSON format 68 | @Bean 69 | MessageConverter jacksonConverter() { 70 | MessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); 71 | return jackson2JsonMessageConverter; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /common/src/main/java/dev/nano/security/JwtAuthConverter.java: -------------------------------------------------------------------------------- 1 | package dev.nano.security; 2 | 3 | import lombok.NonNull; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.security.authentication.AbstractAuthenticationToken; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.oauth2.jwt.Jwt; 10 | import org.springframework.security.oauth2.jwt.JwtClaimNames; 11 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 12 | import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.util.Collection; 16 | import java.util.Collections; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.Stream; 21 | 22 | @Component 23 | public class JwtAuthConverter implements Converter { 24 | 25 | private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); 26 | 27 | @Value("${jwt.auth.converter.principle-attribute}") 28 | private String principleAttribute; 29 | 30 | @Override 31 | public AbstractAuthenticationToken convert(@NonNull Jwt jwt) { 32 | Stream jwtAuthoritiesStream = Optional.of(jwtGrantedAuthoritiesConverter.convert(jwt)) 33 | .stream().flatMap(Collection::stream); 34 | 35 | Collection authorities = Stream.concat( 36 | jwtAuthoritiesStream, 37 | extractRealmRoles(jwt).stream() 38 | ).collect(Collectors.toSet()); 39 | 40 | return new JwtAuthenticationToken(jwt, authorities, getPrincipleClaimName(jwt)); 41 | } 42 | 43 | private String getPrincipleClaimName(Jwt jwt) { 44 | String claimName = JwtClaimNames.SUB; 45 | if (principleAttribute != null) { 46 | claimName = principleAttribute; 47 | } 48 | return jwt.getClaim(claimName); 49 | } 50 | 51 | private Collection extractRealmRoles(Jwt jwt) { 52 | Map realmAccess = jwt.getClaim("realm_access"); 53 | 54 | if (realmAccess == null || realmAccess.isEmpty()) { 55 | return Collections.emptySet(); 56 | } 57 | 58 | Object rolesObject = realmAccess.get("roles"); 59 | 60 | if (!(rolesObject instanceof Collection rawRoles)) { 61 | return Collections.emptySet(); 62 | } 63 | 64 | Collection roles = rawRoles.stream() 65 | .filter(String.class::isInstance) 66 | .map(String.class::cast) 67 | .collect(Collectors.toSet()); 68 | 69 | return roles.stream() 70 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) 71 | .collect(Collectors.toSet()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | import dev.nano.notification.email.EmailService; 5 | import dev.nano.exceptionhandler.business.NotificationException; 6 | import dev.nano.exceptionhandler.core.ResourceNotFoundException; 7 | import lombok.AllArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | import static dev.nano.notification.NotificationConstant.*; 15 | 16 | @Service 17 | @AllArgsConstructor 18 | @Slf4j 19 | public class NotificationServiceImpl implements NotificationService { 20 | 21 | private final NotificationRepository notificationRepository; 22 | private final NotificationMapper notificationMapper; 23 | private final EmailService emailService; 24 | 25 | @Override 26 | public NotificationDTO getNotification(Long notificationId) { 27 | return notificationRepository.findById(notificationId) 28 | .map(notificationMapper::toDTO) 29 | .orElseThrow(() -> new ResourceNotFoundException( 30 | String.format(NOTIFICATION_NOT_FOUND, notificationId) 31 | )); 32 | } 33 | 34 | @Override 35 | public List getAllNotification() { 36 | List notifications = notificationRepository.findAll(); 37 | if (notifications.isEmpty()) { 38 | throw new ResourceNotFoundException(NO_NOTIFICATIONS_FOUND); 39 | } 40 | return notificationMapper.toListDTO(notifications); 41 | } 42 | 43 | @Override 44 | public void sendNotification(NotificationRequest notificationRequest) { 45 | try { 46 | NotificationEntity notification = NotificationEntity.builder() 47 | .customerId(notificationRequest.getCustomerId()) 48 | .customerName(notificationRequest.getCustomerName()) 49 | .customerEmail(notificationRequest.getCustomerEmail()) 50 | .sender("nanodev") 51 | .message(notificationRequest.getMessage()) 52 | .sentAt(LocalDateTime.now()) 53 | .build(); 54 | 55 | notificationRepository.save(notification); 56 | 57 | emailService.send( 58 | notificationRequest.getCustomerEmail(), 59 | buildEmail(notificationRequest.getCustomerName(), notificationRequest.getMessage()), 60 | notificationRequest.getSender() 61 | ); 62 | } catch (Exception e) { 63 | log.error("Failed to send notification: {}", e.getMessage()); 64 | throw new NotificationException(String.format(NOTIFICATION_SEND_ERROR, e.getMessage())); 65 | } 66 | } 67 | 68 | private String buildEmail(String name, String message) { 69 | return String.format(""" 70 | Mail sent to: %s 71 |

%s

72 | """, name, message); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/config/ApiKeyAuthFilter.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order.config; 2 | 3 | import dev.nano.clients.apiKeyManager.apiKey.ApiKeyManagerClient; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.authority.AuthorityUtils; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.filter.OncePerRequestFilter; 16 | 17 | import java.io.IOException; 18 | 19 | @Component 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class ApiKeyAuthFilter extends OncePerRequestFilter { 23 | 24 | private static final String API_KEY_HEADER = "X-API-KEY"; 25 | private static final String APPLICATION_NAME = "ORDER"; 26 | 27 | private final ApiKeyManagerClient apiKeyManagerClient; 28 | 29 | @Override 30 | protected void doFilterInternal(@NonNull HttpServletRequest request, 31 | @NonNull HttpServletResponse response, 32 | @NonNull FilterChain filterChain) throws ServletException, IOException { 33 | 34 | String requestApiKey = request.getHeader(API_KEY_HEADER); 35 | 36 | if (requestApiKey != null) { 37 | log.info("Received request with API Key: {}", requestApiKey); 38 | try { 39 | boolean isAuthorized = apiKeyManagerClient.isKeyAuthorizedForApplication(requestApiKey, APPLICATION_NAME); 40 | log.info("API Key '{}' authorization for application '{}': {}", requestApiKey, APPLICATION_NAME, isAuthorized); // Log the authorization result 41 | if (isAuthorized) { 42 | // If the API Key is valid, authenticate the request as an internal service call 43 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( 44 | "internal-service", 45 | null, 46 | AuthorityUtils.createAuthorityList("ROLE_INTERNAL_SERVICE") 47 | ); 48 | SecurityContextHolder.getContext().setAuthentication(authentication); 49 | log.info("Successfully authenticated request with API Key."); 50 | } else { 51 | log.warn("API Key '{}' is NOT authorized for application '{}'.", requestApiKey, APPLICATION_NAME); 52 | } 53 | } catch (Exception e) { 54 | log.error("Error during API Key authorization for key '{}' and application '{}': {}", requestApiKey, APPLICATION_NAME, e.getMessage(), e); 55 | } 56 | } else { 57 | log.debug("No X-API-KEY header found in the request."); 58 | } 59 | 60 | filterChain.doFilter(request, response); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import dev.nano.exceptionhandler.business.ProductException; 4 | import dev.nano.exceptionhandler.core.ResourceNotFoundException; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | import static dev.nano.product.ProductConstant.*; 14 | 15 | @Service 16 | @AllArgsConstructor 17 | public class ProductServiceImpl implements ProductService { 18 | 19 | private final ProductRepository productRepository; 20 | private final ProductMapper productMapper; 21 | 22 | @Override 23 | public ProductDTO getProduct(long productId) { 24 | return productRepository.findById(productId) 25 | .map(productMapper::toDTO) 26 | .orElseThrow(() -> new ResourceNotFoundException( 27 | String.format(PRODUCT_NOT_FOUND, productId) 28 | )); 29 | } 30 | 31 | @Override 32 | public List getAllProducts(int page, int limit, String search) { 33 | if(page > 0) page = page - 1; 34 | 35 | Pageable pageableRequest = PageRequest.of(page, limit); 36 | Page productPage; 37 | 38 | if(search == null || search.isEmpty()) { 39 | productPage = productRepository.findAllProducts(pageableRequest); 40 | } else { 41 | productPage = productRepository.findAllProductsByCriteria(pageableRequest, search); 42 | } 43 | 44 | List products = productPage.getContent(); 45 | if (products.isEmpty()) { 46 | throw new ResourceNotFoundException(NO_PRODUCTS_FOUND); 47 | } 48 | 49 | return productMapper.toListDTO(products); 50 | } 51 | 52 | @Override 53 | public ProductDTO create(ProductDTO productDTO) { 54 | try { 55 | ProductEntity product = productMapper.toEntity(productDTO); 56 | return productMapper.toDTO(productRepository.save(product)); 57 | } catch (Exception e) { 58 | throw new ProductException(String.format(PRODUCT_CREATE_ERROR, e.getMessage())); 59 | } 60 | } 61 | 62 | @Override 63 | public ProductDTO update(long id, ProductDTO productDTO) { 64 | ProductEntity existingProduct = productRepository.findById(id) 65 | .orElseThrow(() -> new ResourceNotFoundException( 66 | String.format(PRODUCT_NOT_FOUND, id) 67 | )); 68 | 69 | existingProduct.setName(productDTO.getName()); 70 | existingProduct.setPrice(productDTO.getPrice()); 71 | existingProduct.setImage(productDTO.getImage()); 72 | existingProduct.setAvailableQuantity(productDTO.getAvailableQuantity()); 73 | 74 | return productMapper.toDTO(productRepository.save(existingProduct)); 75 | } 76 | 77 | @Override 78 | public void delete(long id) { 79 | if (!productRepository.existsById(id)) { 80 | throw new ResourceNotFoundException(String.format(PRODUCT_NOT_FOUND, id)); 81 | } 82 | try { 83 | productRepository.deleteById(id); 84 | } catch (Exception e) { 85 | throw new ProductException(String.format(PRODUCT_DELETE_ERROR, e.getMessage())); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /eureka-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | eureka-server 13 | 14 | 15 | 16 | org.springframework.cloud 17 | spring-cloud-starter-netflix-eureka-server 18 | 19 | 20 | io.micrometer 21 | micrometer-tracing-bridge-brave 22 | 23 | 24 | io.zipkin.reporter2 25 | zipkin-reporter-brave 26 | 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-maven-plugin 33 | 34 | 35 | 36 | 37 | 38 | jib-build-push-image-to-local 39 | 40 | false 41 | 42 | 43 | 44 | 45 | com.google.cloud.tools 46 | jib-maven-plugin 47 | ${jib.maven.plugin.version} 48 | 49 | 50 | openjdk:17 51 | 52 | 53 | arm64 54 | linux 55 | 56 | 57 | 58 | 59 | ${image} 60 | 61 | latest 62 | 63 | 64 | 65 | 66 | -Dspring.profiles.active=docker 67 | 68 | 69 | 70 | 71 | 72 | package 73 | 74 | dockerBuild 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | # See the Prerequisites section of https://www.rabbitmq.com/cluster-formation.html#peer-discovery-k8s. 3 | kind: StatefulSet 4 | metadata: 5 | name: rabbitmq 6 | spec: 7 | serviceName: rabbitmq 8 | # Three nodes is the recommended minimum. Some features may require a majority of nodes 9 | # to be available. 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: rabbitmq 14 | template: 15 | metadata: 16 | labels: 17 | app: rabbitmq 18 | spec: 19 | serviceAccountName: rabbitmq 20 | terminationGracePeriodSeconds: 10 21 | nodeSelector: 22 | # Use Linux nodes in a mixed OS kubernetes cluster. 23 | # Learn more at https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-os 24 | kubernetes.io/os: linux 25 | containers: 26 | - name: rabbitmq-k8s 27 | image: rabbitmq:3.9.20-management-alpine 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/rabbitmq 31 | # Learn more about what ports various protocols use 32 | # at https://www.rabbitmq.com/networking.html#ports 33 | ports: 34 | - name: http 35 | protocol: TCP 36 | containerPort: 15672 37 | - name: amqp 38 | protocol: TCP 39 | containerPort: 5672 40 | livenessProbe: 41 | exec: 42 | # This is just an example. There is no "one true health check" but rather 43 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 44 | # and intrusive health checks. 45 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 46 | # 47 | # Stage 2 check: 48 | command: ["rabbitmq-diagnostics", "status"] 49 | initialDelaySeconds: 60 50 | # See https://www.rabbitmq.com/monitoring.html for monitoring frequency recommendations. 51 | periodSeconds: 60 52 | timeoutSeconds: 15 53 | readinessProbe: 54 | exec: 55 | # This is just an example. There is no "one true health check" but rather 56 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 57 | # and intrusive health checks. 58 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 59 | # 60 | # Stage 1 check: 61 | command: ["rabbitmq-diagnostics", "ping"] 62 | initialDelaySeconds: 20 63 | periodSeconds: 60 64 | timeoutSeconds: 10 65 | imagePullPolicy: Always 66 | env: 67 | - name: MY_POD_NAME 68 | valueFrom: 69 | fieldRef: 70 | apiVersion: v1 71 | fieldPath: metadata.name 72 | - name: MY_POD_NAMESPACE 73 | valueFrom: 74 | fieldRef: 75 | fieldPath: metadata.namespace 76 | - name: RABBITMQ_USE_LONGNAME 77 | value: "true" 78 | # See a note on cluster_formation.k8s.address_type in the config file section 79 | - name: K8S_SERVICE_NAME 80 | value: rabbitmq 81 | - name: RABBITMQ_NODENAME 82 | value: rabbit@$(MY_POD_NAME).$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 83 | - name: K8S_HOSTNAME_SUFFIX 84 | value: .$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 85 | - name: RABBITMQ_ERLANG_COOKIE 86 | value: "mycookie" 87 | volumes: 88 | - name: config-volume 89 | configMap: 90 | name: rabbitmq-config 91 | items: 92 | - key: rabbitmq.conf 93 | path: rabbitmq.conf 94 | - key: enabled_plugins 95 | path: enabled_plugins 96 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | # See the Prerequisites section of https://www.rabbitmq.com/cluster-formation.html#peer-discovery-k8s. 3 | kind: StatefulSet 4 | metadata: 5 | name: rabbitmq 6 | spec: 7 | serviceName: rabbitmq 8 | # Three nodes is the recommended minimum. Some features may require a majority of nodes 9 | # to be available. 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: rabbitmq 14 | template: 15 | metadata: 16 | labels: 17 | app: rabbitmq 18 | spec: 19 | serviceAccountName: rabbitmq 20 | terminationGracePeriodSeconds: 10 21 | nodeSelector: 22 | # Use Linux nodes in a mixed OS kubernetes cluster. 23 | # Learn more at https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-os 24 | kubernetes.io/os: linux 25 | containers: 26 | - name: rabbitmq-k8s 27 | image: rabbitmq:3.9.20-management-alpine 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/rabbitmq 31 | # Learn more about what ports various protocols use 32 | # at https://www.rabbitmq.com/networking.html#ports 33 | ports: 34 | - name: http 35 | protocol: TCP 36 | containerPort: 15672 37 | - name: amqp 38 | protocol: TCP 39 | containerPort: 5672 40 | livenessProbe: 41 | exec: 42 | # This is just an example. There is no "one true health check" but rather 43 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 44 | # and intrusive health checks. 45 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 46 | # 47 | # Stage 2 check: 48 | command: ["rabbitmq-diagnostics", "status"] 49 | initialDelaySeconds: 120 50 | # See https://www.rabbitmq.com/monitoring.html for monitoring frequency recommendations. 51 | periodSeconds: 60 52 | timeoutSeconds: 30 53 | failureThreshold: 5 54 | readinessProbe: 55 | exec: 56 | # This is just an example. There is no "one true health check" but rather 57 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 58 | # and intrusive health checks. 59 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 60 | # 61 | # Stage 1 check: 62 | command: ["rabbitmq-diagnostics", "ping"] 63 | initialDelaySeconds: 60 64 | periodSeconds: 60 65 | timeoutSeconds: 20 66 | failureThreshold: 5 67 | imagePullPolicy: IfNotPresent 68 | env: 69 | - name: MY_POD_NAME 70 | valueFrom: 71 | fieldRef: 72 | apiVersion: v1 73 | fieldPath: metadata.name 74 | - name: MY_POD_NAMESPACE 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: metadata.namespace 78 | - name: RABBITMQ_USE_LONGNAME 79 | value: "true" 80 | # See a note on cluster_formation.k8s.address_type in the config file section 81 | - name: K8S_SERVICE_NAME 82 | value: rabbitmq 83 | - name: RABBITMQ_NODENAME 84 | value: rabbit@$(MY_POD_NAME).$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 85 | - name: K8S_HOSTNAME_SUFFIX 86 | value: .$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 87 | - name: RABBITMQ_ERLANG_COOKIE 88 | value: "mycookie" 89 | volumes: 90 | - name: config-volume 91 | configMap: 92 | name: rabbitmq-config 93 | items: 94 | - key: rabbitmq.conf 95 | path: rabbitmq.conf 96 | - key: enabled_plugins 97 | path: enabled_plugins 98 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import dev.nano.amqp.RabbitMQProducer; 4 | import dev.nano.clients.notification.NotificationRequest; 5 | import dev.nano.clients.order.OrderRequest; 6 | import dev.nano.clients.product.ProductClient; 7 | import dev.nano.exceptionhandler.business.NotificationException; 8 | import dev.nano.exceptionhandler.business.OrderException; 9 | import dev.nano.exceptionhandler.core.ResourceNotFoundException; 10 | import feign.FeignException; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Service; 14 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 15 | 16 | import java.time.LocalDateTime; 17 | import java.util.List; 18 | 19 | import static dev.nano.order.OrderConstant.*; 20 | 21 | @Service 22 | @AllArgsConstructor 23 | @Slf4j 24 | public class OrderServiceImpl implements OrderService { 25 | private final OrderRepository orderRepository; 26 | private final OrderMapper orderMapper; 27 | private final ProductClient productClient; 28 | private final RabbitMQProducer rabbitMQProducer; 29 | 30 | @Override 31 | public OrderDTO getOrder(Long id) { 32 | return orderRepository.findById(id) 33 | .map(orderMapper::toDTO) 34 | .orElseThrow(() -> new ResourceNotFoundException( 35 | String.format(ORDER_NOT_FOUND, id) 36 | )); 37 | } 38 | 39 | @Override 40 | public List getAllOrders() { 41 | List orders = orderRepository.findAll(); 42 | if (orders.isEmpty()) { 43 | throw new ResourceNotFoundException(NO_ORDERS_FOUND); 44 | } 45 | return orderMapper.toListDTO(orders); 46 | } 47 | 48 | @CircuitBreaker(name = "productService", fallbackMethod = "createOrderFallback") 49 | @Override 50 | public OrderDTO createOrder(OrderRequest orderRequest) { 51 | try { 52 | // Verify product exists 53 | productClient.getProduct(orderRequest.getProductId()); 54 | 55 | OrderEntity order = OrderEntity.builder() 56 | .customerId(orderRequest.getCustomerId()) 57 | .productId(orderRequest.getProductId()) 58 | .amount(orderRequest.getAmount()) 59 | .createAt(LocalDateTime.now()) 60 | .build(); 61 | 62 | OrderEntity savedOrder = orderRepository.save(order); 63 | sendOrderNotification(orderRequest); 64 | 65 | return orderMapper.toDTO(savedOrder); 66 | } catch (FeignException e) { 67 | throw new OrderException(String.format(ORDER_CREATE_ERROR, e.getMessage())); 68 | } 69 | } 70 | 71 | private void sendOrderNotification(OrderRequest order) { 72 | try { 73 | NotificationRequest notificationRequest = NotificationRequest.builder() 74 | .customerId(order.getCustomerId()) 75 | .customerName(order.getCustomerName()) 76 | .customerEmail(order.getCustomerEmail()) 77 | .sender("NanoDev") 78 | .message("Your order has been created successfully") 79 | .build(); 80 | 81 | rabbitMQProducer.publish( 82 | "internal.exchange", 83 | "internal.notification.routing-key", 84 | notificationRequest 85 | ); 86 | } catch (Exception e) { 87 | log.error("Failed to send order notification: {}", e.getMessage()); 88 | throw new NotificationException("Failed to send order notification"); 89 | } 90 | } 91 | 92 | private OrderDTO createOrderFallback(OrderRequest orderRequest, Throwable throwable) { 93 | log.error("Fallback triggered for createOrder: {}", throwable.getMessage()); 94 | throw new OrderException("Product service is currently unavailable. Please try again later."); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/controller/ApiKeyController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.controller; 2 | 3 | import dev.nano.apikey.ApiKeyConstant; 4 | import dev.nano.apikey.ApiKeyRequest; 5 | import dev.nano.apikey.ApiKeyService; 6 | import dev.nano.application.ApplicationName; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import io.swagger.v3.oas.annotations.media.Content; 9 | import io.swagger.v3.oas.annotations.media.Schema; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 11 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 12 | import io.swagger.v3.oas.annotations.tags.Tag; 13 | import lombok.AllArgsConstructor; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.PostMapping; 18 | import org.springframework.web.bind.annotation.PutMapping; 19 | import org.springframework.web.bind.annotation.RequestBody; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RestController; 22 | import dev.nano.swagger.BaseController; 23 | 24 | @RestController 25 | @RequestMapping(path = ApiKeyConstant.API_KEY_URI_REST_API) 26 | @Tag(name = BaseController.API_KEY_TAG, description = BaseController.API_KEY_DESCRIPTION) 27 | @AllArgsConstructor 28 | public class ApiKeyController { 29 | private final ApiKeyService apiKeyService; 30 | 31 | @Operation( 32 | summary = "Generate new API key", 33 | description = "Create a new API key with specified client details and permissions" 34 | ) 35 | @ApiResponses(value = { 36 | @ApiResponse( 37 | responseCode = "200", 38 | description = "API key generated successfully", 39 | content = @Content(mediaType = "application/json", schema = @Schema(type = "string")) 40 | ), 41 | @ApiResponse(responseCode = "400", description = "Invalid request data"), 42 | @ApiResponse(responseCode = "500", description = "Internal server error") 43 | }) 44 | @PostMapping 45 | public ResponseEntity generateNewApiKey( 46 | @RequestBody ApiKeyRequest apiKeyRequest) { 47 | return ResponseEntity.ok(apiKeyService.save(apiKeyRequest)); 48 | } 49 | 50 | @Operation( 51 | summary = "Revoke API key", 52 | description = "Disable and revoke an existing API key" 53 | ) 54 | @ApiResponses(value = { 55 | @ApiResponse(responseCode = "200", description = "API key revoked successfully"), 56 | @ApiResponse(responseCode = "404", description = "API key not found"), 57 | @ApiResponse(responseCode = "500", description = "Internal server error") 58 | }) 59 | @PutMapping("{apiKey}/revoke") 60 | public void revokeKey(@PathVariable("apiKey") String apiKey) { 61 | apiKeyService.revokeApi(apiKey); 62 | } 63 | 64 | @Operation( 65 | summary = "Check API key authorization", 66 | description = "Verify if an API key is authorized for a specific application" 67 | ) 68 | @ApiResponses(value = { 69 | @ApiResponse( 70 | responseCode = "200", 71 | description = "Authorization check completed", 72 | content = @Content(mediaType = "application/json", schema = @Schema(type = "boolean")) 73 | ), 74 | @ApiResponse(responseCode = "400", description = "Invalid input parameters"), 75 | @ApiResponse(responseCode = "500", description = "Internal server error") 76 | }) 77 | @GetMapping("{apiKey}/applications/{applicationName}/authorization") 78 | public ResponseEntity isKeyAuthorizedForApplication( 79 | @PathVariable("apiKey") String apiKey, 80 | @PathVariable("applicationName") ApplicationName applicationName) { 81 | return ResponseEntity.ok(apiKeyService.isAuthorized(apiKey, applicationName)); 82 | } 83 | } 84 | --------------------------------------------------------------------------------