├── .gitignore ├── system ├── config │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── configurations │ │ │ │ ├── monitoring.yml │ │ │ │ ├── authentication-service.yml │ │ │ │ ├── gateway.yml │ │ │ │ ├── order-service.yml │ │ │ │ ├── product-service.yml │ │ │ │ └── application.yml │ │ │ └── application.yml │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── config │ │ │ └── Application.java │ ├── Dockerfile │ └── pom.xml ├── gateway │ ├── Dockerfile │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── bootstrap.yml │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── proxy │ │ │ └── Application.java │ └── pom.xml ├── discovery-server │ ├── Dockerfile │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── bootstrap.yml │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── discovery │ │ │ └── Application.java │ └── pom.xml ├── monitoring │ ├── Dockerfile │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── bootstrap.yml │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── monitoring │ │ │ └── Application.java │ └── pom.xml └── authentication │ ├── src │ └── main │ │ ├── resources │ │ ├── bootstrap.yml │ │ └── keystore.jks │ │ └── java │ │ └── demo │ │ └── ecommerce │ │ └── auth │ │ ├── exception │ │ └── EmailAlreadyExists.java │ │ ├── repository │ │ └── UserRepository.java │ │ ├── Application.java │ │ ├── config │ │ ├── ResourceServerConfig.java │ │ ├── DataBaseConfig.java │ │ └── AuthorizationServerConfig.java │ │ ├── service │ │ └── UserService.java │ │ ├── controller │ │ ├── UserController.java │ │ └── JWTKeyEndPoint.java │ │ ├── model │ │ └── User.java │ │ └── security │ │ ├── service │ │ └── ClientDetailsServiceImpl.java │ │ └── model │ │ └── ClientDetailsInfo.java │ ├── Dockerfile │ └── pom.xml ├── storage ├── migration │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── flyway │ │ │ │ └── migrations │ │ │ │ ├── m20190824_1__User_Roles.sql │ │ │ │ ├── m20190824_2__Product_New_Columns.sql │ │ │ │ ├── m20190928_1__New_Prices_Cols.sql │ │ │ │ ├── m20190908_1__Order_Cols.sql │ │ │ │ ├── m20190905_1__Add_Indexes.sql │ │ │ │ └── m20190805__Base_Tables.sql │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── migration │ │ │ └── Application.java │ ├── Dockerfile │ ├── Dockerfile.bkp │ └── pom.xml └── postgresdb │ └── Dockerfile ├── services ├── order │ ├── Dockerfile │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── bootstrap.yml │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── order │ │ │ ├── Application.java │ │ │ ├── controller │ │ │ └── OrderController.java │ │ │ └── service │ │ │ └── OrderService.java │ └── pom.xml └── product │ ├── Dockerfile │ ├── src │ ├── main │ │ ├── resources │ │ │ └── bootstrap.yml │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── product │ │ │ ├── Application.java │ │ │ ├── controller │ │ │ └── ProductController.java │ │ │ └── service │ │ │ └── ProductService.java │ └── test │ │ └── java │ │ └── demo │ │ └── ecommerce │ │ └── product │ │ └── AppTest.java │ └── pom.xml ├── common.env ├── components ├── model │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── demo │ │ │ └── ecommerce │ │ │ └── model │ │ │ ├── util │ │ │ └── Count.java │ │ │ ├── user │ │ │ └── User.java │ │ │ ├── order │ │ │ ├── ShoppingCart.java │ │ │ └── ShoppingCartItem.java │ │ │ └── product │ │ │ └── Product.java │ └── pom.xml └── framework │ ├── src │ └── main │ │ └── java │ │ └── demo │ │ └── ecommerce │ │ ├── repository │ │ ├── user │ │ │ └── UserRepository.java │ │ ├── order │ │ │ ├── ShoppingCartItemRepository.java │ │ │ └── ShoppingCartRepository.java │ │ └── product │ │ │ └── ProductRepository.java │ │ └── config │ │ ├── DatabaseConfig.java │ │ └── SecurityConfig.java │ └── pom.xml ├── docker-commands ├── docker-compose.yml ├── endpoints.md ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | target -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/monitoring.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8000 -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190824_1__User_Roles.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN roles character varying(250) null; -------------------------------------------------------------------------------- /storage/migration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ADD /target/migration-1.0-jar-with-dependencies.jar migration.jar 3 | ENTRYPOINT java -jar migration.jar migration -------------------------------------------------------------------------------- /services/order/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8081 3 | EXPOSE 8081 4 | ADD /target/order-1.0.jar order.jar 5 | ENTRYPOINT java $EXTRA_JAR_ARGS -jar order.jar -------------------------------------------------------------------------------- /system/config/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8111 3 | EXPOSE 8111:8111 4 | ADD /target/config-1.0.jar config.jar 5 | ENTRYPOINT java -jar config.jar config -------------------------------------------------------------------------------- /system/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8765 3 | EXPOSE 8765:8765 4 | ADD /target/gateway-1.0.jar gateway.jar 5 | ENTRYPOINT java -jar gateway.jar gateway -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/authentication-service.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8087 3 | 4 | keystore: 5 | password: admin123 6 | keyPairAlias: ecommerce 7 | -------------------------------------------------------------------------------- /system/discovery-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 2222 3 | EXPOSE 2222:2222 4 | ADD /target/service-discovery-1.0.jar eureka.jar 5 | ENTRYPOINT java -jar eureka.jar eureka -------------------------------------------------------------------------------- /system/monitoring/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8000 3 | EXPOSE 8000:8000 4 | ADD /target/monitoring-1.0.jar monitoring.jar 5 | ENTRYPOINT java -jar monitoring.jar monitoring -------------------------------------------------------------------------------- /services/product/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8080 3 | EXPOSE 8080:8080 4 | ADD /target/product-1.0.jar product.jar 5 | ENTRYPOINT java $EXTRA_JAR_ARGS -jar product.jar product -------------------------------------------------------------------------------- /services/order/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: order-service 4 | cloud: 5 | config: 6 | uri: ${CONFIG_SERVICE_URL} 7 | fail-fast: true -------------------------------------------------------------------------------- /system/gateway/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: gateway 4 | cloud: 5 | config: 6 | uri: ${CONFIG_SERVICE_URL} 7 | fail-fast: true 8 | -------------------------------------------------------------------------------- /system/authentication/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: authentication-service 4 | cloud: 5 | config: 6 | uri: http://config-server:8111 7 | 8 | -------------------------------------------------------------------------------- /system/monitoring/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: monitoring 4 | cloud: 5 | config: 6 | uri: ${CONFIG_SERVICE_URL} 7 | fail-fast: true 8 | -------------------------------------------------------------------------------- /system/authentication/src/main/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mostafacs/ecommerce-microservices-spring-reactive-webflux/HEAD/system/authentication/src/main/resources/keystore.jks -------------------------------------------------------------------------------- /system/authentication/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ENV port = 8087 3 | EXPOSE 8087:8087 4 | ADD /target/authentication-1.0.jar authentication.jar 5 | ENTRYPOINT java -jar $EXTRA_JAR_ARGS authentication.jar authentication -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/gateway.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8765 3 | 4 | management: 5 | endpoint: 6 | gateway: 7 | enabled: true 8 | 9 | endpoints: 10 | web: 11 | exposure: 12 | include: gateway 13 | -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190824_2__Product_New_Columns.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE product 2 | ADD COLUMN user_id bigint null; 3 | 4 | ALTER TABLE product 5 | ADD COLUMN created_on date null; 6 | 7 | ALTER TABLE product 8 | ADD COLUMN updated_on date null; 9 | -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/order-service.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | spring: 5 | security: 6 | oauth2: 7 | resourceserver: 8 | jwt: 9 | #issuer-uri: http://localhost:8087/ 10 | jwk-set-uri: ${JWKS_URL} -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/product-service.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | security: 6 | oauth2: 7 | resourceserver: 8 | jwt: 9 | #issuer-uri: http://localhost:8087/ 10 | jwk-set-uri: ${JWKS_URL} -------------------------------------------------------------------------------- /services/product/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: product-service 4 | cloud: 5 | config: 6 | uri: ${CONFIG_SERVICE_URL} 7 | fail-fast: true 8 | # password: ${CONFIG_SERVICE_PASSWORD} 9 | # username: user 10 | -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190928_1__New_Prices_Cols.sql: -------------------------------------------------------------------------------- 1 | Alter TABLE shopping_cart_item ADD Column unit_cost_price numeric(18,5); 2 | 3 | Alter TABLE product ADD Column cost_price numeric(18,5); 4 | Alter TABLE product ADD Column sell_price numeric(18,5); -------------------------------------------------------------------------------- /common.env: -------------------------------------------------------------------------------- 1 | CONFIG_SERVICE_URL=http://config-server:8111 2 | DISCOVERY_SERVER_URL=http://discovery-server:2222/eureka/ 3 | JWKS_URL=http://authentication-server:8087/.well-known/jwks 4 | DATABASE_HOST=postgresdb 5 | DATABASE_NAME=ecommerce 6 | DATABASE_SCHEMA=ecommerce 7 | DATABASE_PORT=5432 8 | DATABASE_USER=ecommerce 9 | DATABASE_PASSWORD=admin123 -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/exception/EmailAlreadyExists.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.exception; 2 | 3 | /** 4 | * @author Mostafa Albana 5 | */ 6 | public class EmailAlreadyExists extends Exception { 7 | @Override 8 | public String getMessage() { 9 | return "This email already exists"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/model/src/main/java/demo/ecommerce/model/util/Count.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.model.util; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @author Mostafa Albana 9 | */ 10 | 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Data 14 | public class Count { 15 | Long count; 16 | } 17 | -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190908_1__Order_Cols.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE shopping_cart 2 | ADD COLUMN created_on date null; 3 | 4 | ALTER TABLE shopping_cart 5 | ADD COLUMN updated_on date null; 6 | 7 | CREATE INDEX idx_cart_order_created_on ON shopping_cart USING btree (created_on); 8 | CREATE INDEX idx_cart_order_updated_on ON shopping_cart USING btree (updated_on); 9 | -------------------------------------------------------------------------------- /system/config/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: config 4 | cloud: 5 | config: 6 | server: 7 | native: 8 | search-locations: classpath:/configurations 9 | profiles: 10 | active: native 11 | # security: 12 | # user: 13 | # password: ${CONFIG_SERVICE_PASSWORD} 14 | 15 | server: 16 | port: 8111 17 | -------------------------------------------------------------------------------- /services/product/src/test/java/demo/ecommerce/product/AppTest.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.product; 2 | 3 | //import static org.junit.Assert.assertTrue; 4 | // 5 | //import org.junit.Test; 6 | // 7 | ///** 8 | // * Unit test for simple App. 9 | // */ 10 | //public class AppTest 11 | //{ 12 | // /** 13 | // * Rigorous Test :-) 14 | // */ 15 | // @Test 16 | // public void shouldAnswerWithTrue() 17 | // { 18 | // assertTrue( true ); 19 | // } 20 | //} 21 | -------------------------------------------------------------------------------- /system/config/src/main/resources/configurations/application.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | service-url: 4 | defaultZone: ${DISCOVERY_SERVER_URL} 5 | instance: 6 | leaseRenewalIntervalInSeconds: 1 7 | leaseExpirationDurationInSeconds: 2 8 | 9 | logging: 10 | level: 11 | org.springframework.security: INFO 12 | 13 | hystrix: 14 | command: 15 | default: 16 | execution: 17 | isolation: 18 | thread: 19 | timeoutInMilliseconds: 10000 -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190905_1__Add_Indexes.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX idx_product_sku ON product USING btree (sku); 2 | CREATE INDEX idx_product_user_id ON product USING btree (user_id); 3 | CREATE INDEX idx_product_created_on ON product USING btree (created_on); 4 | CREATE INDEX idx_product_updated_on ON product USING btree (updated_on); 5 | 6 | 7 | CREATE INDEX idx_sc_user_id ON shopping_cart USING btree (user_id); 8 | 9 | CREATE INDEX idx_users_email ON users USING btree (email); -------------------------------------------------------------------------------- /system/discovery-server/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 2222 3 | 4 | spring: 5 | application: 6 | name: discovery-service 7 | 8 | 9 | eureka: 10 | instance: 11 | hostname: localhost 12 | nonSecurePort: 2222 13 | client: 14 | register-with-eureka: false 15 | fetch-registry: false 16 | # service-url: 17 | # default-zone: http://localhost:2222/eureka/ 18 | server: 19 | wait-time-in-ms-when-sync-empty: 0 20 | enable-self-preservation: false 21 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/repository/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.repository.user; 2 | 3 | import demo.ecommerce.model.user.User; 4 | import org.springframework.data.r2dbc.repository.query.Query; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import reactor.core.publisher.Mono; 7 | 8 | public interface UserRepository extends ReactiveCrudRepository { 9 | 10 | @Query("Select * from users where email=$1") 11 | Mono getUserByEmail(String email); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /system/config/src/main/java/demo/ecommerce/config/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.config; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.config.server.EnableConfigServer; 6 | 7 | /** 8 | * @author Mostafa Albana 9 | */ 10 | 11 | @SpringBootApplication 12 | @EnableConfigServer 13 | public class Application { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /system/monitoring/src/main/java/demo/ecommerce/monitoring/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.monitoring; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; 6 | 7 | /** 8 | * @author Mostafa Albana 9 | */ 10 | 11 | @SpringBootApplication 12 | @EnableHystrixDashboard 13 | public class Application { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.repository; 2 | 3 | import demo.ecommerce.auth.model.User; 4 | import org.springframework.data.jdbc.repository.query.Query; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.data.repository.query.Param; 7 | 8 | /** 9 | * @author Mostafa Albana 10 | */ 11 | 12 | public interface UserRepository extends CrudRepository { 13 | 14 | @Query("select * from users where email = :email") 15 | User findByEmail(@Param("email") String email); 16 | } 17 | -------------------------------------------------------------------------------- /system/discovery-server/src/main/java/demo/ecommerce/discovery/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.discovery; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; 6 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 7 | 8 | @SpringBootApplication(exclude = { GsonAutoConfiguration.class }) 9 | @EnableEurekaServer 10 | public class Application { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /storage/postgresdb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11 2 | MAINTAINER Mostafa Albana 3 | 4 | EXPOSE 5432:5433 5 | ENV PGDATA /var/lib/postgresql-static/data 6 | VOLUME ["$PGDATA"] 7 | RUN mkdir -p $PGDATA 8 | 9 | ENV DATABASE_SCHEMA=ecommerce 10 | ENV DATABASE_PORT=5432 11 | ENV DATABASE_USER=ecommerce 12 | ENV DATABASE_PASSWORD=admin123 13 | 14 | RUN echo "psql -U postgres --command \"CREATE ROLE $DATABASE_USER LOGIN PASSWORD '$DATABASE_PASSWORD' SUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION;\" && \ 15 | psql -U postgres --command \" CREATE DATABASE $DATABASE_SCHEMA WITH OWNER $DATABASE_USER ENCODING 'UTF8';\" "> /docker-entrypoint-initdb.d/setup.sh 16 | 17 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/repository/order/ShoppingCartItemRepository.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.repository.order; 2 | 3 | import demo.ecommerce.model.order.ShoppingCartItem; 4 | import org.springframework.data.r2dbc.repository.query.Query; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import reactor.core.publisher.Flux; 7 | 8 | /** 9 | * @Author Mostafa Albana 10 | */ 11 | 12 | public interface ShoppingCartItemRepository extends ReactiveCrudRepository { 13 | 14 | @Query("Select * from shopping_cart_item where shopping_cart_id = $1") 15 | Flux getShoppingOrderCartItems(Long cartId); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /components/model/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | ecommerce-microservices 8 | demo.ecommerce 9 | 1.0 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | demo.ecommerce 16 | model 17 | 1.0 18 | 19 | 20 | -------------------------------------------------------------------------------- /services/product/src/main/java/demo/ecommerce/product/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.product; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | 8 | /** 9 | * @Author Mostafa 10 | */ 11 | 12 | @SpringBootApplication 13 | @ComponentScan({"demo.ecommerce"}) 14 | public class Application 15 | { 16 | 17 | @Bean 18 | String[] publicEndpoints() { 19 | return new String[]{}; 20 | } 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(Application.class, args); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/repository/order/ShoppingCartRepository.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.repository.order; 2 | 3 | import demo.ecommerce.model.order.ShoppingCart; 4 | import org.springframework.data.r2dbc.repository.query.Query; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import reactor.core.publisher.Flux; 7 | 8 | /** 9 | * @Author Mostafa Albana 10 | */ 11 | 12 | public interface ShoppingCartRepository extends ReactiveCrudRepository { 13 | 14 | @Query("Select * from shopping_cart where user_id = $1 order by updated_on desc, id desc LIMIT $2 OFFSET $3") 15 | Flux getUserShoppingCartsPageable(Long userId, int limit, long offset); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /services/order/src/main/java/demo/ecommerce/order/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.order; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | 8 | 9 | /** 10 | * @Author Mostafa Albana 11 | */ 12 | 13 | @SpringBootApplication 14 | @ComponentScan({"demo.ecommerce"}) 15 | public class Application { 16 | 17 | // all endpoints is secure. 18 | @Bean 19 | String[] publicEndpoints() { 20 | return new String[]{}; 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(Application.class); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /components/framework/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ecommerce-microservices 7 | demo.ecommerce 8 | 1.0 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | framework 14 | 15 | 16 | 17 | demo.ecommerce 18 | model 19 | 1.0 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth; 2 | 3 | 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 7 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 8 | 9 | @SpringBootApplication 10 | @EnableAuthorizationServer 11 | @EnableResourceServer 12 | public class Application { 13 | public static void main( String[] args ) 14 | { 15 | // curl admin:123@localhost:8087/oauth/token -d grant_type=client_credentials 16 | // curl admin%40admin.com:123@localhost:8087/oauth/token -d grant_type=client_credentials 17 | SpringApplication.run(Application.class, args); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 6 | 7 | @Configuration 8 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 9 | 10 | 11 | private static final String[] PUBLIC_RESOURCES = { 12 | 13 | "/.well-known**", 14 | "/user**", 15 | "/user/**", 16 | "/sign-key/public", 17 | "/oauth/token", 18 | "/oauth/check_token", 19 | 20 | }; 21 | 22 | 23 | 24 | @Override 25 | public void configure(HttpSecurity http) throws Exception { 26 | http.anonymous().and().authorizeRequests().antMatchers(PUBLIC_RESOURCES).permitAll() 27 | .antMatchers("/admin/**").hasRole("admin").anyRequest().authenticated(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /storage/migration/Dockerfile.bkp: -------------------------------------------------------------------------------- 1 | 2 | FROM postgres:11 3 | MAINTAINER Mostafa Albana 4 | EXPOSE 5432:5433 5 | 6 | 7 | ENV PGDATA /var/lib/postgresql-static/data 8 | VOLUME ["$PGDATA"] 9 | RUN mkdir -p $PGDATA 10 | 11 | RUN apt-get update && \ 12 | apt-get -y install software-properties-common 13 | 14 | #Install JDK 15 | RUN apt-get update \ 16 | && apt-get install -y openjdk-8-jdk 17 | 18 | 19 | ADD /target/postgresdb-1.0-jar-with-dependencies.jar postgresdb.jar 20 | ENV database_name=ecommerce 21 | ENV schema_name=ecommerce 22 | ENV database_url=jdbc:postgresql://localhost:5432/ecommerce 23 | ENV user_name=ecommerce 24 | ENV user_password=admin123 25 | 26 | RUN echo "psql -U postgres --command \"CREATE ROLE $user_name LOGIN PASSWORD '$user_password' SUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION;\" && \ 27 | psql -U postgres --command \" CREATE DATABASE $database_name WITH OWNER $user_name ENCODING 'UTF8';\" && \ 28 | pg_ctl -D $PGDATA stop && \ 29 | pg_ctl -D $PGDATA start && \ 30 | java -jar postgresdb.jar " > /docker-entrypoint-initdb.d/setup.sh 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/service/UserService.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.service; 2 | 3 | import demo.ecommerce.auth.exception.EmailAlreadyExists; 4 | import demo.ecommerce.auth.model.User; 5 | import demo.ecommerce.auth.repository.UserRepository; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.Date; 11 | 12 | /** 13 | * @author Mostafa Albana 14 | */ 15 | 16 | @Service 17 | public class UserService { 18 | 19 | @Autowired 20 | UserRepository userRepository; 21 | 22 | @Autowired 23 | BCryptPasswordEncoder encoder; 24 | 25 | public User createUser(User user) throws EmailAlreadyExists { 26 | User exist = userRepository.findByEmail(user.getEmail()); 27 | if (exist != null) { 28 | throw new EmailAlreadyExists(); 29 | } 30 | Date now = new Date(); 31 | user.setPassword(encoder.encode(user.getPassword())); 32 | user.setCreateDate(now); 33 | user.setUpdateDate(now); 34 | return userRepository.save(user); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/repository/product/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.repository.product; 2 | 3 | import demo.ecommerce.model.product.Product; 4 | import org.springframework.data.r2dbc.repository.query.Query; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import reactor.core.publisher.Flux; 7 | 8 | public interface ProductRepository extends ReactiveCrudRepository { 9 | 10 | @Query("Select * from product order by updated_on desc, id desc LIMIT $1 OFFSET $2") 11 | Flux findAllPageable(int limit, long offset); 12 | 13 | @Query("Select * from product where inventory_count > 0 order by updated_on desc, id desc LIMIT $1 OFFSET $2") 14 | Flux availableProductsPageable(int limit, long offset); 15 | 16 | @Query("Select * from product where inventory_count = 0 order by updated_on desc, id desc LIMIT $1 OFFSET $2") 17 | Flux notAvailableProductsPageable(int limit, long offset); 18 | 19 | @Query("Select * from product where user_id=$1 order by updated_on desc, id desc LIMIT $2 OFFSET $3") 20 | Flux userProductsPageable(Long userId, int limit, long offset); 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /components/model/src/main/java/demo/ecommerce/model/user/User.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.model.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.relational.core.mapping.Column; 9 | import org.springframework.data.relational.core.mapping.Table; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * @author Mostafa Albana 15 | */ 16 | 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | @Data 20 | @Table("users") 21 | public class User { 22 | @Id 23 | private Long id; 24 | 25 | private String email; 26 | 27 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 28 | private String password; 29 | 30 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 31 | private String roles; 32 | 33 | @Column("first_name") 34 | private String firstName; 35 | 36 | @Column("last_name") 37 | private String lastName; 38 | 39 | @Column("create_date") 40 | private Date createDate; 41 | 42 | @Column("update_date") 43 | private Date updateDate; 44 | 45 | @Column("last_login") 46 | private Date lastLogin; 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.controller; 2 | 3 | import demo.ecommerce.auth.model.User; 4 | import demo.ecommerce.auth.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.security.Principal; 12 | 13 | @RestController 14 | public class UserController { 15 | 16 | @Autowired 17 | UserService userService; 18 | 19 | @GetMapping("/user") 20 | public Principal getLoginUser(Principal principal) { 21 | return principal; 22 | } 23 | 24 | @PostMapping("/user/merchant") 25 | public User merchantRegister(@RequestBody User user) throws Exception { 26 | user.setRoles("merchant"); 27 | return userService.createUser(user); 28 | } 29 | 30 | @PostMapping("/user/client") 31 | public User clientRegister(@RequestBody User user) throws Exception { 32 | user.setRoles("client"); 33 | return userService.createUser(user); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/model/User.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.relational.core.mapping.Column; 9 | import org.springframework.data.relational.core.mapping.Table; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * @author Mostafa Albana 15 | */ 16 | 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | @Data 20 | @Table("users") 21 | public class User { 22 | @Id 23 | private Long id; 24 | 25 | private String email; 26 | 27 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 28 | private String password; 29 | 30 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 31 | private String roles; 32 | 33 | @Column("first_name") 34 | private String firstName; 35 | 36 | @Column("last_name") 37 | private String lastName; 38 | 39 | @Column("create_date") 40 | private Date createDate; 41 | 42 | @Column("update_date") 43 | private Date updateDate; 44 | 45 | @Column("last_login") 46 | private Date lastLogin; 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/security/service/ClientDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.security.service; 2 | 3 | import demo.ecommerce.auth.model.User; 4 | import demo.ecommerce.auth.repository.UserRepository; 5 | import demo.ecommerce.auth.security.model.ClientDetailsInfo; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.security.oauth2.provider.ClientDetails; 9 | import org.springframework.security.oauth2.provider.ClientDetailsService; 10 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 11 | import org.springframework.stereotype.Service; 12 | 13 | /** 14 | * @author Mostafa Albana 15 | */ 16 | 17 | @Service 18 | public class ClientDetailsServiceImpl implements ClientDetailsService { 19 | 20 | @Autowired 21 | UserRepository userRepository; 22 | 23 | @Autowired 24 | BCryptPasswordEncoder encode; 25 | 26 | @Override 27 | 28 | public ClientDetails loadClientByClientId(String id) throws ClientRegistrationException { 29 | User user = userRepository.findByEmail(id.trim()); 30 | if (user == null) return null; 31 | return new ClientDetailsInfo(user); 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /components/model/src/main/java/demo/ecommerce/model/order/ShoppingCart.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.model.order; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.data.annotation.Id; 7 | import org.springframework.data.annotation.Transient; 8 | import org.springframework.data.relational.core.mapping.Column; 9 | import org.springframework.data.relational.core.mapping.Table; 10 | 11 | import java.util.Date; 12 | import java.util.List; 13 | 14 | /** 15 | * @Author Mostafa Albana 16 | */ 17 | 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | @Table("shopping_cart") 22 | public class ShoppingCart { 23 | 24 | @Id 25 | private Long id; 26 | 27 | @Column("total_quantity") 28 | private Integer totalQuantity; 29 | 30 | // totalCost - shipping 31 | @Column("sub_total_price") 32 | private Double subTotalPrice; 33 | 34 | @Column("total_shipping_cost") 35 | private Double totalShippingCost; 36 | 37 | @Column("total_cost") 38 | private Double totalCost; 39 | 40 | @Column("user_id") 41 | private Long userId; 42 | 43 | @Column("created_on") 44 | private Date createdOn; 45 | 46 | @Column("updated_on") 47 | private Date updatedOn; 48 | 49 | @Transient 50 | private List cartItemList; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /components/model/src/main/java/demo/ecommerce/model/product/Product.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.model.product; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.annotation.Transient; 9 | import org.springframework.data.relational.core.mapping.Column; 10 | import org.springframework.data.relational.core.mapping.Table; 11 | 12 | import java.util.Date; 13 | 14 | 15 | @Data 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Table("product") 19 | public class Product { 20 | 21 | @Id 22 | private Long id; 23 | 24 | private String sku; 25 | 26 | private String title; 27 | 28 | @Column("cost_price") 29 | private Double costPrice; 30 | 31 | @Column("sell_price") 32 | private Double sellPrice; 33 | 34 | 35 | @Column("inventory_count") 36 | private Integer inventoryCounts; 37 | 38 | @Column("user_id") 39 | private Long userId; 40 | 41 | @Column("created_on") 42 | private Date createdOn; 43 | 44 | @Column("updated_on") 45 | private Date updatedOn; 46 | 47 | @Transient 48 | public void increaseInventory(int amount) { 49 | inventoryCounts += amount; 50 | } 51 | 52 | @Transient 53 | public void decreaseInventory(int amount) { 54 | inventoryCounts -= amount; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /components/model/src/main/java/demo/ecommerce/model/order/ShoppingCartItem.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.model.order; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import demo.ecommerce.model.product.Product; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.data.annotation.Id; 10 | import org.springframework.data.annotation.Transient; 11 | import org.springframework.data.relational.core.mapping.Column; 12 | import org.springframework.data.relational.core.mapping.Table; 13 | 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @Table("shopping_cart_item") 18 | public class ShoppingCartItem { 19 | 20 | @Id 21 | private Long id; 22 | 23 | @Transient 24 | private Product product; 25 | 26 | @JsonIgnore 27 | @Column("product_id") 28 | private Long productId; 29 | 30 | @JsonIgnore 31 | @Column("shopping_cart_id") 32 | private Long shoppingCartId; 33 | 34 | private Integer quantity; 35 | 36 | // product cost price 37 | @JsonIgnore 38 | @Column("unit_cost_price") 39 | private Double unitCostPrice; 40 | 41 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 42 | // product selling price 43 | @Column("unit_price") 44 | private Double unitPrice; 45 | 46 | @JsonProperty(access = JsonProperty.Access.READ_ONLY) 47 | // (unit-price * quantity) + shipping 48 | @Column("total_price") 49 | private Double totalPrice; 50 | 51 | // calculated from any external sources 52 | @Column("shipping_cost") 53 | private Double shippingCost = 0.0; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /system/gateway/src/main/java/demo/ecommerce/proxy/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.proxy; 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.gateway.route.RouteLocator; 7 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | /** 11 | * @author Mostafa Albana 12 | */ 13 | 14 | @EnableDiscoveryClient 15 | @SpringBootApplication 16 | public class Application { 17 | public static void main(String[] args) { 18 | SpringApplication.run(Application.class); 19 | } 20 | 21 | @Bean 22 | public RouteLocator routeLocator(RouteLocatorBuilder builder) { 23 | return builder.routes() 24 | .route(r -> r.path("/uaa/**") 25 | .filters(f -> f.rewritePath("/uaa/(?.*)", "/$\\{path}")) 26 | .uri("lb://authentication-service") 27 | .id("authentication-service")) 28 | .route(r -> r.path("/product/**") 29 | .filters(f -> f.rewritePath("/product/(?.*)", "/$\\{path}")) 30 | .uri("lb://product-service") 31 | .id("product-service")) 32 | .route(r -> r.path("/order/**") 33 | .filters(f -> f.rewritePath("/order/(?.*)", "/$\\{path}")) 34 | .uri("lb://order-service") 35 | .id("order-service")) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services/product/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | ecommerce-microservices 7 | demo.ecommerce 8 | 1.0 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | product 14 | 15 | product 16 | 17 | 18 | 19 | 20 | demo.ecommerce 21 | framework 22 | 1.0 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-maven-plugin 32 | 2.1.3.RELEASE 33 | 34 | demo.ecommerce.product.Application 35 | ZIP 36 | 37 | 38 | 39 | 40 | repackage 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /services/order/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | demo.ecommerce 8 | order 9 | 1.0 10 | 11 | 12 | ecommerce-microservices 13 | demo.ecommerce 14 | 1.0 15 | ../../pom.xml 16 | 17 | 18 | 19 | 20 | demo.ecommerce 21 | framework 22 | 1.0 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-maven-plugin 31 | 2.1.3.RELEASE 32 | 33 | demo.ecommerce.order.Application 34 | ZIP 35 | 36 | 37 | 38 | 39 | repackage 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/config/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.config; 2 | 3 | import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; 4 | import io.r2dbc.postgresql.PostgresqlConnectionFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; 9 | import org.springframework.data.r2dbc.function.DatabaseClient; 10 | import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; 11 | 12 | @Configuration 13 | @EnableR2dbcRepositories(basePackages = {"demo.ecommerce.repository.**"}) 14 | public class DatabaseConfig extends AbstractR2dbcConfiguration { 15 | 16 | @Value("${DATABASE_HOST}") 17 | String hostName; 18 | 19 | @Value("${DATABASE_NAME}") 20 | String database; 21 | 22 | @Value("${DATABASE_SCHEMA}") 23 | String schema; 24 | 25 | @Value("${DATABASE_USER}") 26 | String username; 27 | 28 | @Value("${DATABASE_PASSWORD}") 29 | String password; 30 | 31 | @Value("${DATABASE_PORT}") 32 | int port; 33 | 34 | @Bean 35 | @Override 36 | public PostgresqlConnectionFactory connectionFactory() { 37 | return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() 38 | .host(hostName) 39 | .database(database) 40 | .schema(schema) 41 | .port(port) 42 | .username(username) 43 | .password(password).build()); 44 | } 45 | 46 | 47 | 48 | @Bean("databaseClient") 49 | public DatabaseClient dataBaseClient() { 50 | return DatabaseClient.create(connectionFactory()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /storage/migration/src/main/java/demo/ecommerce/migration/Application.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.migration; 2 | 3 | import org.flywaydb.core.Flyway; 4 | 5 | /** 6 | * @author Mostafa Albana 7 | */ 8 | public class Application { 9 | 10 | private static final String hostEnvName = "DATABASE_HOST"; 11 | private static final String portEnvName = "DATABASE_PORT"; 12 | private static final String schemaEnvName = "DATABASE_SCHEMA"; 13 | private static final String userEnvName = "DATABASE_USER"; 14 | private static final String passwordEnvName = "DATABASE_PASSWORD"; 15 | 16 | public static void main(String[] args) throws Exception { 17 | 18 | 19 | String host = System.getenv(hostEnvName); 20 | String port = System.getenv(portEnvName); 21 | String schema = System.getenv(schemaEnvName); 22 | String user = System.getenv(userEnvName); 23 | String password = System.getenv(passwordEnvName); 24 | String url = String.format("jdbc:postgresql://%s:%s/%s", host, port, schema); 25 | 26 | System.out.println(url); 27 | System.out.println(schema); 28 | System.out.println(user); 29 | System.out.println(password); 30 | 31 | String path = Application.class.getClassLoader().getResource("flyway/migrations").getFile().replace("file:", "filesystem:"); 32 | System.out.println(path); 33 | Flyway flyway = Flyway.configure(). 34 | locations("flyway/migrations"). 35 | dataSource(url, user, password). 36 | sqlMigrationPrefix("m"). 37 | sqlMigrationSeparator("__") 38 | .schemas(schema) 39 | .load(); 40 | 41 | flyway.migrate(); 42 | System.out.println(".................... Migration Completed successfully ................."); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docker-commands: -------------------------------------------------------------------------------- 1 | ------------- Docker compose build & Run ---------- 2 | docker-compose build 3 | docker-compose up 4 | docker-compose up discovery-server 5 | docker-compose up authentication-server 6 | docker-compose up product-service 7 | .. 8 | 9 | # Docker compose Stop All Services 10 | docker-compose stop 11 | docker-compose stop product-service 12 | 13 | ------------ Docker --------- 14 | # Open bash 15 | docker run -it postgresdb /bin/bash 16 | docker run -it authentication-server /bin/bash 17 | docker run -it discovery-server /bin/bash 18 | 19 | # Docker Build commands 20 | docker build --tag postgresdb . 21 | docker build --tag authentication-server . 22 | docker build --tag product-service . 23 | docker build --tag order-service . 24 | 25 | # Docker Run Commands 26 | docker run -p 5433:5432 postgresdb 27 | docker run -p 8087:8087 authentication-server 28 | docker run -p 8080:8080 product-service 29 | docker run -p 8081:8081 order-service 30 | 31 | # Clear all Docker 32 | docker rm $(docker ps -a -q) -f 33 | docker volume prune 34 | 35 | # force rebuild 36 | docker build --no-cache --tag postgresdb . 37 | 38 | # Register 39 | 40 | curl -i -X POST \ 41 | -H "Content-Type:application/json" \ 42 | -d \ 43 | '{ 44 | "email": "client", 45 | "firstName": "Mostafa", 46 | "lastName": "Albana", 47 | "password": "123" 48 | }' \ 49 | 'http://localhost:8087/user/client' 50 | 51 | -------------------------------------------------------------- 52 | 53 | curl -i -X POST \ 54 | -H "Content-Type:application/json" \ 55 | -d \ 56 | '{ 57 | "email": "merchant", 58 | "firstName": "Mostafa", 59 | "lastName": "Albana", 60 | "password": "123" 61 | }' \ 62 | 'http://localhost:8087/user/merchant' 63 | 64 | 65 | # Login 66 | curl client:123@localhost:8087/oauth/token -d grant_type=client_credentials 67 | curl merchant:123@localhost:8087/oauth/token -d grant_type=client_credentials 68 | 69 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/controller/JWTKeyEndPoint.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.controller; 2 | 3 | import com.nimbusds.jose.jwk.JWK; 4 | import com.nimbusds.jose.jwk.KeyUse; 5 | import com.nimbusds.jose.jwk.RSAKey; 6 | import net.minidev.json.JSONObject; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.core.io.ClassPathResource; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint; 11 | import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.ResponseBody; 14 | 15 | import java.security.KeyPair; 16 | import java.security.interfaces.RSAPrivateKey; 17 | import java.security.interfaces.RSAPublicKey; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | /** 22 | * @Author Mostafa Albana 23 | */ 24 | 25 | @FrameworkEndpoint 26 | public class JWTKeyEndPoint { 27 | 28 | 29 | @Value("${keystore.password}") 30 | String keyStorePassword; 31 | 32 | @Value("${keystore.keyPairAlias}") 33 | String keyPairAlias; 34 | 35 | 36 | @GetMapping(value = "/.well-known/jwks") 37 | @ResponseBody 38 | public ResponseEntity key() { 39 | KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), keyStorePassword.toCharArray()); 40 | KeyPair keypair = keyStoreKeyFactory.getKeyPair(keyPairAlias); 41 | 42 | JWK jwk = new RSAKey.Builder((RSAPublicKey) keypair.getPublic()) 43 | .privateKey((RSAPrivateKey) keypair.getPrivate()) 44 | .keyUse(KeyUse.SIGNATURE) 45 | .keyID("resource") 46 | .build(); 47 | 48 | return ResponseEntity.ok(new Jwts(Arrays.asList(jwk.toJSONObject()))); 49 | } 50 | 51 | 52 | class Jwts { 53 | public List keys; 54 | 55 | public Jwts(List keys) { 56 | this.keys = keys; 57 | } 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /system/config/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | demo.ecommerce 8 | config 9 | 1.0 10 | 11 | 12 | UTF-8 13 | 1.8 14 | 1.8 15 | 1.8 16 | Greenwich.RELEASE 17 | 18 | 19 | 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-dependencies 25 | ${spring-cloud.version} 26 | pom 27 | import 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.cloud 36 | spring-cloud-config-server 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-maven-plugin 45 | 2.1.3.RELEASE 46 | 47 | demo.ecommerce.config.Application 48 | ZIP 49 | 50 | 51 | 52 | 53 | repackage 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/config/DataBaseConfig.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.config; 2 | 3 | import org.apache.commons.dbcp.BasicDataSource; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.annotation.Order; 8 | import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; 9 | import org.springframework.data.jdbc.repository.config.JdbcConfiguration; 10 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; 11 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 12 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 13 | import org.springframework.transaction.PlatformTransactionManager; 14 | 15 | import javax.sql.DataSource; 16 | 17 | /** 18 | * @author Mostafa Albana 19 | */ 20 | 21 | @Configuration 22 | @EnableJdbcRepositories("demo.ecommerce.auth.repository") 23 | //@Import(AbstractJdbcConfiguration.class) 24 | @Order(-558484) 25 | public class DataBaseConfig extends JdbcConfiguration { 26 | 27 | @Value("${DATABASE_HOST}") 28 | String hostName; 29 | 30 | @Value("${DATABASE_NAME}") 31 | String database; 32 | 33 | @Value("${DATABASE_SCHEMA}") 34 | String schema; 35 | 36 | @Value("${DATABASE_USER}") 37 | String username; 38 | 39 | @Value("${DATABASE_PASSWORD}") 40 | String password; 41 | 42 | @Value("${DATABASE_PORT}") 43 | int port; 44 | 45 | @Bean 46 | NamedParameterJdbcOperations operations() { 47 | return new NamedParameterJdbcTemplate(dataSource()); 48 | } 49 | 50 | @Bean 51 | PlatformTransactionManager transactionManager() { 52 | return new DataSourceTransactionManager(dataSource()); 53 | } 54 | 55 | 56 | @Bean 57 | public DataSource dataSource() { 58 | BasicDataSource dataSource = new BasicDataSource(); 59 | dataSource.setUrl(String.format("jdbc:postgresql://%s:%d/%s?currentSchema=%s", hostName, port, database, schema)); 60 | dataSource.setUsername(username); 61 | dataSource.setPassword(password); 62 | dataSource.setDriverClassName("org.postgresql.Driver"); 63 | dataSource.setInitialSize(5); 64 | dataSource.setMaxActive(12); 65 | return dataSource; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /system/discovery-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 4.0.0 7 | 8 | demo.ecommerce 9 | service-discovery 10 | 1.0 11 | service-discovery 12 | 13 | 14 | UTF-8 15 | 1.8 16 | 1.8 17 | 1.8 18 | Greenwich.RELEASE 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-dependencies 26 | ${spring-cloud.version} 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-netflix-eureka-server 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-config-client 42 | 2.1.4.RELEASE 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 2.1.3.RELEASE 53 | 54 | demo.ecommerce.discovery.Application 55 | ZIP 56 | 57 | 58 | 59 | 60 | repackage 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | 5 | postgresdb: 6 | build: storage/postgresdb 7 | env_file: 8 | - common.env 9 | ports: 10 | - 5433:5432 11 | 12 | config-server: 13 | build: system/config 14 | ports: 15 | - 8111:8111 16 | 17 | migration: 18 | build: storage/migration 19 | env_file: 20 | - common.env 21 | ports: 22 | - 9999:9999 23 | depends_on: 24 | - postgresdb 25 | links: 26 | - postgresdb 27 | 28 | 29 | discovery-server: 30 | build: system/discovery-server 31 | env_file: 32 | - common.env 33 | ports: 34 | - 2222:2222 35 | depends_on: 36 | - config-server 37 | links: 38 | - config-server 39 | 40 | 41 | authentication-server: 42 | build: system/authentication 43 | env_file: 44 | - common.env 45 | environment: 46 | EXTRA_JAR_ARGS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8187 47 | ports: 48 | - 8087:8087 49 | - 8187:8187 #debug port 50 | depends_on: 51 | - postgresdb 52 | - discovery-server 53 | links: 54 | - config-server 55 | - postgresdb 56 | - discovery-server 57 | 58 | 59 | product-service: 60 | build: services/product 61 | env_file: 62 | - common.env 63 | environment: 64 | EXTRA_JAR_ARGS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8180 65 | ports: 66 | - 8080:8080 67 | - 8180:8180 #debug port 68 | depends_on: 69 | - authentication-server 70 | links: 71 | - config-server 72 | - discovery-server 73 | - postgresdb 74 | - authentication-server 75 | 76 | 77 | order-service: 78 | build: services/order 79 | env_file: 80 | - common.env 81 | environment: 82 | EXTRA_JAR_ARGS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8181 83 | ports: 84 | - 8081:8081 85 | - 8181:8181 86 | depends_on: 87 | - authentication-server 88 | links: 89 | - config-server 90 | - discovery-server 91 | - postgresdb 92 | - authentication-server 93 | 94 | 95 | gateway: 96 | build: system/gateway 97 | env_file: 98 | - common.env 99 | ports: 100 | - 8765:8765 101 | depends_on: 102 | - product-service 103 | - order-service 104 | links: 105 | - config-server 106 | - discovery-server 107 | 108 | monitor: 109 | build: system/monitoring 110 | env_file: 111 | - common.env 112 | ports: 113 | - 8000:8000 114 | depends_on: 115 | - discovery-server 116 | links: 117 | - config-server 118 | - discovery-server 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /services/product/src/main/java/demo/ecommerce/product/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.product.controller; 2 | 3 | import demo.ecommerce.model.product.Product; 4 | import demo.ecommerce.product.service.ProductService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 11 | import org.springframework.web.bind.annotation.*; 12 | import reactor.core.publisher.Mono; 13 | 14 | @RestController 15 | public class ProductController { 16 | 17 | @Autowired 18 | ProductService productService; 19 | 20 | 21 | @GetMapping("/list") 22 | public Mono> getProducts(@RequestParam Integer page, @RequestParam Integer pageSize) { 23 | return productService.findAllProductsPaged(PageRequest.of(page, pageSize)); 24 | } 25 | 26 | @PreAuthorize("hasAnyAuthority('SCOPE_merchant', 'SCOPE_admin')") 27 | @GetMapping("/list/merchant") 28 | public Mono> getMerchantProducts(JwtAuthenticationToken auth, @RequestParam Integer page, @RequestParam Integer pageSize) { 29 | String email = auth.getTokenAttributes().get("client_id").toString(); 30 | return productService.findUserProductsPaged(email, PageRequest.of(page, pageSize)); 31 | } 32 | 33 | // Not available for clients 34 | @PreAuthorize("hasAnyAuthority('SCOPE_merchant', 'SCOPE_admin')") 35 | @PostMapping("/save") 36 | public Mono saveProduct(JwtAuthenticationToken auth, @RequestBody Product product) { 37 | String email = auth.getTokenAttributes().get("client_id").toString(); 38 | return productService.saveProduct(product, email); 39 | } 40 | 41 | // Not available for clients 42 | @PreAuthorize("hasAnyAuthority('SCOPE_merchant', 'SCOPE_admin')") 43 | @PutMapping("/save") 44 | public Mono updateProduct(Authentication auth, @RequestBody Product product) { 45 | if (product.getId() == null) 46 | throw new IllegalArgumentException("Product id is required to update existing product"); 47 | return productService.saveProduct(product, product.getUserId()); 48 | } 49 | 50 | @GetMapping("/list/available") 51 | public Mono> getNonZeroInventoryProducts(@RequestParam Integer page, @RequestParam Integer pageSize) { 52 | return productService.findAvailableProductsPaged(PageRequest.of(page, pageSize)); 53 | } 54 | 55 | @GetMapping("/list/not-available") 56 | public Mono> getZeroInventoryProducts(@RequestParam Integer page, @RequestParam Integer pageSize) { 57 | return productService.findNotAvailableProductsPaged(PageRequest.of(page, pageSize)); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/security/model/ClientDetailsInfo.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.security.model; 2 | 3 | import demo.ecommerce.auth.model.User; 4 | import org.apache.commons.lang.StringUtils; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.oauth2.provider.ClientDetails; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * @author Mostafa Albana 13 | */ 14 | public class ClientDetailsInfo implements ClientDetails { 15 | 16 | private User user; 17 | 18 | public ClientDetailsInfo(User user) { 19 | this.user = user; 20 | } 21 | 22 | @Override 23 | public String getClientId() { 24 | return user.getEmail(); 25 | } 26 | 27 | @Override 28 | public Set getResourceIds() { 29 | return null; 30 | } 31 | 32 | @Override 33 | public boolean isSecretRequired() { 34 | return false; 35 | } 36 | 37 | @Override 38 | public String getClientSecret() { 39 | return user.getPassword(); 40 | } 41 | 42 | @Override 43 | public boolean isScoped() { 44 | return false; 45 | } 46 | 47 | // Actually used by webflux security filter. 48 | @Override 49 | public Set getScope() { 50 | final String[] roles = user.getRoles().split(","); 51 | return new HashSet<>(Arrays.asList(roles)); 52 | } 53 | 54 | @Override 55 | public Set getAuthorizedGrantTypes() { 56 | return new HashSet<>(Arrays.asList("client_credentials")); 57 | } 58 | 59 | @Override 60 | public Set getRegisteredRedirectUri() { 61 | return null; 62 | } 63 | 64 | @Override 65 | public Collection getAuthorities() { 66 | 67 | if (StringUtils.isNotEmpty(user.getRoles())) { 68 | List gRoles = new ArrayList<>(); 69 | final String[] roles = user.getRoles().split(","); 70 | for (String role : roles) { 71 | gRoles.add(new SimpleGrantedAuthority(role)); 72 | } 73 | 74 | return gRoles; 75 | } 76 | return null; 77 | } 78 | 79 | @Override 80 | public Integer getAccessTokenValiditySeconds() { 81 | return 100000; 82 | } 83 | 84 | @Override 85 | public Integer getRefreshTokenValiditySeconds() { 86 | return null; 87 | } 88 | 89 | @Override 90 | public boolean isAutoApprove(String s) { 91 | return true; 92 | } 93 | 94 | @Override 95 | public Map getAdditionalInformation() { 96 | HashMap additionInformation = new HashMap<>(); 97 | additionInformation.put("userId", user.getId()); 98 | return additionInformation; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /system/monitoring/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | demo.ecommerce 8 | monitoring 9 | 1.0 10 | 11 | 12 | UTF-8 13 | 1.8 14 | 2.1.1.RELEASE 15 | 1.8 16 | 1.8 17 | Greenwich.RELEASE 18 | 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-dependencies 26 | ${spring-cloud.version} 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-netflix-hystrix-dashboard 38 | 39 | 40 | 41 | org.springframework.cloud 42 | spring-cloud-starter-netflix-eureka-client 43 | 44 | 45 | 46 | org.springframework.cloud 47 | spring-cloud-config-client 48 | 49 | 50 | 51 | 52 | com.google.code.gson 53 | gson 54 | 2.8.5 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-maven-plugin 64 | 2.1.3.RELEASE 65 | 66 | demo.ecommerce.monitoring.Application 67 | ZIP 68 | 69 | 70 | 71 | 72 | repackage 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /system/authentication/src/main/java/demo/ecommerce/auth/config/AuthorizationServerConfig.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.auth.config; 2 | 3 | import demo.ecommerce.auth.security.service.ClientDetailsServiceImpl; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.io.ClassPathResource; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 10 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 12 | import org.springframework.security.oauth2.provider.ClientDetailsService; 13 | import org.springframework.security.oauth2.provider.token.TokenStore; 14 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 15 | import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; 16 | import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; 17 | 18 | import java.security.KeyPair; 19 | 20 | 21 | /** 22 | * @Author Mostafa Albana 23 | */ 24 | 25 | @Configuration 26 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 27 | 28 | 29 | @Override 30 | public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception { 31 | clientDetailsServiceConfigurer.withClientDetails(userDetailsService()); 32 | } 33 | 34 | 35 | @Override 36 | public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 37 | security.passwordEncoder(encoder()); 38 | } 39 | 40 | @Override 41 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 42 | endpoints.tokenStore(tokenStore()) 43 | .accessTokenConverter(accessTokenConverter()); 44 | } 45 | 46 | 47 | @Bean 48 | public TokenStore tokenStore() { 49 | return new JwtTokenStore(accessTokenConverter()); 50 | } 51 | 52 | @Bean 53 | public JwtAccessTokenConverter accessTokenConverter() { 54 | KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "admin123".toCharArray()); 55 | KeyPair keyPair = keyStoreKeyFactory.getKeyPair("ecommerce"); 56 | JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); 57 | converter.setKeyPair(keyPair); 58 | return converter; 59 | } 60 | 61 | 62 | @Bean 63 | public BCryptPasswordEncoder encoder() { 64 | return new BCryptPasswordEncoder(); 65 | } 66 | 67 | @Bean 68 | public ClientDetailsService userDetailsService() { 69 | return new ClientDetailsServiceImpl(); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /endpoints.md: -------------------------------------------------------------------------------- 1 | 2 | # Endpoints 3 | 4 | ## Authentication 5 | 6 | ### Register as client 7 | ``` 8 | curl -i -X POST \ 9 | -H "Content-Type:application/json" \ 10 | -d \ 11 | '{ 12 | "email": "client@gmail.com", 13 | "firstName": "Mostafa", 14 | "lastName": "Albana", 15 | "password": "123" 16 | }' \ 17 | 'http://localhost:8765/uaa/user/client' 18 | ``` 19 | 20 | ### Register as Merchant 21 | 22 | ``` 23 | curl -i -X POST \ 24 | -H "Content-Type:application/json" \ 25 | -d \ 26 | '{ 27 | "email": "merchant@gmail.com", 28 | "firstName": "Mostafa", 29 | "lastName": "Albana", 30 | "password": "123" 31 | }' \ 32 | 'http://localhost:8765/uaa/user/merchant' 33 | ``` 34 | 35 | ### Login 36 | ``` 37 | curl client%40gmail.com:123@localhost:8087/oauth/token -d grant_type=client_credentials 38 | 39 | curl merchant%40gmail.com:123@localhost:8087/oauth/token -d grant_type=client_credentials 40 | ``` 41 | 42 | ##<<< NOTE >>> 43 | Please replace ${token} with active token you get after merchant login 44 | 45 | ## Product 46 | 47 | ---- Create or update (Just specify id to update) (Only merchant role allowed) 48 | ``` 49 | curl -i -X POST \ 50 | -H "Authorization:Bearer ${token}" \ 51 | -H "Content-Type:application/json" \ 52 | -d \ 53 | '{ 54 | "sku":"ps4-pro-99", 55 | "title":"PlayStation 4 Pro", 56 | "costPrice":25.5, 57 | "sellPrice": 32.2, 58 | "inventoryCounts":2 59 | }' \ 60 | 'http://localhost:8765/product/save' 61 | ``` 62 | 63 | 64 | --- List Products created by Merchant (Only merchant role allowed) 65 | ``` 66 | curl -i -X GET \ 67 | -H "Authorization:Bearer ${token}" \ 68 | 'http://localhost:8765/product/list/merchant?page=0&pageSize=2' 69 | ``` 70 | --- List products with quantity more than 0 71 | ``` 72 | curl -i -X GET \ 73 | -H "Authorization:Bearer ${token}" \ 74 | 'http://localhost:8765/product/list/available?page=0&pageSize=2' 75 | ``` 76 | 77 | --- List products with quantity equal to 0 78 | ``` 79 | curl -i -X GET \ 80 | -H "Authorization:Bearer ${token}" \ 81 | 'http://localhost:8765/product/list/not-available?page=0&pageSize=2' 82 | ``` 83 | ## Order 84 | --- Create Order (Only Client Role Allowed) 85 | ``` 86 | curl -i -X POST \ 87 | -H "Authorization:Bearer ${token}" \ 88 | -H "Content-Type:application/json" \ 89 | -d \ 90 | '{ 91 | 92 | "cartItemList": [ 93 | {"product": {"id": 4}, "quantity": 1} 94 | ] 95 | }' \ 96 | 'http://localhost:8765/order/save' 97 | ``` 98 | --- update order 99 | ``` 100 | curl -i -X PUT \ 101 | -H "Authorization:Bearer ${token}" \ 102 | -H "Content-Type:application/json" \ 103 | -d \ 104 | '{ 105 | 106 | "cartItemList": [ 107 | "id": 26 108 | {"product": {"id": 4}, "quantity": 1} 109 | ] 110 | }' \ 111 | 'http://localhost:8765/order/save' 112 | ``` 113 | 114 | --- Get user orders (Only Client Role Allowed) 115 | ``` 116 | curl -i -X GET \ 117 | -H "Authorization:Bearer ${token}" \ 118 | 'http://localhost:8765/order/list/user' 119 | ``` 120 | 121 | --- Get order by id (Only Client Role Allowed) 122 | replace ${orderId} with your order id 123 | ``` 124 | curl -i -X GET \ 125 | -H "Authorization:Bearer ${token}" \ 126 | 'http://localhost:8765/order/${orderId}' 127 | ``` 128 | -------------------------------------------------------------------------------- /services/order/src/main/java/demo/ecommerce/order/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.order.controller; 2 | 3 | import demo.ecommerce.model.order.ShoppingCart; 4 | import demo.ecommerce.order.service.OrderService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.access.prepost.PreAuthorize; 11 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 12 | import org.springframework.web.bind.annotation.*; 13 | import reactor.core.publisher.Mono; 14 | 15 | @RestController 16 | public class OrderController { 17 | 18 | @Autowired 19 | OrderService orderService; 20 | 21 | 22 | /** 23 | * Mono return serialization error 24 | * Because: ServerResponse is the HTTP response type used by Spring WebFlux.fn, the functional variant of the reactive web framework. 25 | * it'snot supposed to use it within an annotated controller. 26 | * Solution use ResponseEntity 27 | */ 28 | @PreAuthorize("hasAnyAuthority('SCOPE_client')") 29 | @PostMapping("/save") // save 30 | public Mono saveOrder(@RequestBody ShoppingCart shoppingCart, JwtAuthenticationToken auth) { 31 | String email = auth.getTokenAttributes().get("client_id").toString(); 32 | return orderService.saveShoppingCart(shoppingCart, email). 33 | flatMap(cart -> Mono.just(ResponseEntity.ok(cart))) 34 | .cast(ResponseEntity.class) 35 | .onErrorResume(ex -> Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()))); 36 | } 37 | 38 | @PutMapping("/save") 39 | public Mono updateOrder(JwtAuthenticationToken auth, @RequestBody ShoppingCart shoppingCart) { 40 | 41 | if (shoppingCart.getId() == null) 42 | throw new IllegalArgumentException("Cart id is required to update existing cart"); 43 | String email = auth.getTokenAttributes().get("client_id").toString(); 44 | return orderService.saveShoppingCart(shoppingCart, email). 45 | flatMap(cart -> Mono.just(ResponseEntity.ok(cart))) 46 | .cast(ResponseEntity.class) 47 | .onErrorResume(ex -> { 48 | ex.printStackTrace(); 49 | return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage())); 50 | }); 51 | } 52 | 53 | @PreAuthorize("hasAnyAuthority('SCOPE_client')") 54 | @GetMapping("/{orderId}") 55 | Mono getOrder(@PathVariable("orderId") Long orderId, JwtAuthenticationToken auth) { 56 | String email = auth.getTokenAttributes().get("client_id").toString(); 57 | return orderService.getShoppingCart(orderId, email); 58 | } 59 | 60 | @PreAuthorize("hasAnyAuthority('SCOPE_client')") 61 | @GetMapping("/list/user") 62 | Mono> getAllShoppingCart(JwtAuthenticationToken auth, @RequestParam Integer page, @RequestParam Integer pageSize) { 63 | String email = auth.getTokenAttributes().get("client_id").toString(); 64 | return orderService.getUserShoppingCarts(email, PageRequest.of(page, pageSize)); 65 | } 66 | 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /system/gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | demo.ecommerce 8 | gateway 9 | 1.0 10 | 11 | 12 | UTF-8 13 | 1.8 14 | 2.1.1.RELEASE 15 | 1.8 16 | 1.8 17 | Greenwich.RELEASE 18 | 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-dependencies 26 | ${spring-cloud.version} 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-gateway 38 | 39 | 40 | 41 | org.springframework.cloud 42 | spring-cloud-starter-netflix-eureka-client 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-actuator 48 | 2.1.3.RELEASE 49 | 50 | 51 | 52 | org.springframework.cloud 53 | spring-cloud-starter-netflix-hystrix 54 | 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-config-client 59 | 60 | 61 | 62 | com.google.code.gson 63 | gson 64 | 2.8.5 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 2.1.3.RELEASE 74 | 75 | demo.ecommerce.proxy.Application 76 | ZIP 77 | 78 | 79 | 80 | 81 | repackage 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /components/framework/src/main/java/demo/ecommerce/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; 6 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 7 | import org.springframework.security.config.web.server.ServerHttpSecurity; 8 | import org.springframework.security.web.server.SecurityWebFilterChain; 9 | import org.springframework.util.Assert; 10 | import org.springframework.web.server.adapter.HttpWebHandlerAdapter; 11 | 12 | 13 | /** 14 | * @Author Mostafa Albana 15 | */ 16 | 17 | @EnableWebFluxSecurity 18 | @EnableReactiveMethodSecurity 19 | public class SecurityConfig { 20 | 21 | // Defined in each Service use this config file. 22 | @Autowired 23 | String[] publicEndpoints; 24 | 25 | @Bean 26 | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { 27 | 28 | Assert.notNull(publicEndpoints, "No bean with name publicEndPoints defined"); 29 | 30 | if (publicEndpoints.length > 0) { 31 | HttpWebHandlerAdapter a; 32 | http.authorizeExchange().pathMatchers(publicEndpoints).permitAll(); 33 | } 34 | 35 | http.csrf().disable().cors().disable().authorizeExchange() 36 | .pathMatchers("/product/save").hasAuthority("SCOPE_merchant") 37 | .pathMatchers("/**").authenticated() 38 | .and() 39 | .oauth2ResourceServer().jwt(); //.jwtAuthenticationConverter(grantedAuthoritiesExtractor()); 40 | 41 | return http.build(); 42 | } 43 | 44 | 45 | /* Converter> grantedAuthoritiesExtractor() { 46 | GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor(); 47 | return new ReactiveJwtAuthenticationConverterAdapter(extractor); 48 | } 49 | 50 | 51 | static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter { 52 | protected Collection extractAuthorities(Jwt jwt) { 53 | Collection authorities = (Collection) 54 | jwt.getClaims().get("authorities"); 55 | 56 | return authorities.stream() 57 | .map(SimpleGrantedAuthority::new) 58 | .collect(Collectors.toList()); 59 | } 60 | }*/ 61 | 62 | // static class GrantedAuthoritiesExtractor implements Converter> { 63 | // 64 | // @Override 65 | // public final Mono convert(Jwt jwt) { 66 | // Collection authorities = this.extractAuthorities(jwt); 67 | // return Mono.just(new JwtAuthenticationToken(jwt, authorities)); 68 | // } 69 | // 70 | // 71 | // protected Collection extractAuthorities(Jwt jwt) { 72 | // Collection authorities = ((Collection) 73 | // jwt.getClaims().get("authorities")).stream().map(itm -> "SCOPE_" + itm).collect(Collectors.toList()); 74 | // 75 | // return authorities.stream() 76 | // .map(SimpleGrantedAuthority::new) 77 | // .collect(Collectors.toList()); 78 | // } 79 | // } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /services/product/src/main/java/demo/ecommerce/product/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.product.service; 2 | 3 | import demo.ecommerce.model.product.Product; 4 | import demo.ecommerce.repository.product.ProductRepository; 5 | import demo.ecommerce.repository.user.UserRepository; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.PageImpl; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.data.r2dbc.function.DatabaseClient; 11 | import org.springframework.stereotype.Service; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | 18 | /** 19 | * @Author Mostafa 20 | */ 21 | 22 | @Service 23 | public class ProductService { 24 | 25 | @Autowired 26 | ProductRepository productRepository; 27 | 28 | @Autowired 29 | UserRepository userRepository; 30 | 31 | @Autowired 32 | DatabaseClient db; 33 | 34 | 35 | public Mono saveProduct(Product product, String email) { 36 | return userRepository.getUserByEmail(email).flatMap(user -> saveProduct(product, user.getId())); 37 | } 38 | 39 | public Mono saveProduct(Product product, Long userId) { 40 | if (product.getId() == null) { 41 | product.setCreatedOn(new Date()); 42 | } 43 | product.setUpdatedOn(new Date()); 44 | product.setUserId(userId); 45 | return productRepository.save(product); 46 | } 47 | 48 | public Mono> findAllProductsPaged(Pageable pageable) { 49 | Mono> products = productRepository.findAllPageable(pageable.getPageSize(), pageable.getOffset()).collectList(); 50 | Mono totalProductsCount = productRepository.count(); 51 | return products.flatMap(productList -> 52 | totalProductsCount.flatMap(totalCount -> Mono.just(new PageImpl(productList, pageable, totalCount))) 53 | ); 54 | } 55 | 56 | public Mono> findNotAvailableProductsPaged(Pageable pageable) { 57 | return productRepository.notAvailableProductsPageable(pageable.getPageSize(), pageable.getOffset()).collectList().flatMap( 58 | products -> countNotAvailableProducts().flatMap(count -> Mono.just(new PageImpl(products, pageable, count))) 59 | ); 60 | } 61 | 62 | public Mono> findAvailableProductsPaged(Pageable pageable) { 63 | return productRepository.availableProductsPageable(pageable.getPageSize(), pageable.getOffset()).collectList().flatMap(products -> 64 | countAvailableProducts().flatMap(count -> Mono.just(new PageImpl(products, pageable, count)))); 65 | } 66 | 67 | public Mono> findUserProductsPaged(String email, Pageable pageable) { 68 | return userRepository.getUserByEmail(email).flatMap(user -> 69 | productRepository.userProductsPageable(user.getId(), pageable.getPageSize(), pageable.getOffset()).collectList().flatMap(products -> 70 | 71 | countUserProducts(user.getId()).flatMap(count -> Mono.just(new PageImpl(products, pageable, count))) 72 | )); 73 | } 74 | 75 | // REF -> https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#reference 76 | 77 | Mono countNotAvailableProducts() { 78 | return db.execute().sql("Select count(id) as total from product where inventory_count = 0").map((row, rowMetadata) -> row.get("total", Long.class)).one(); 79 | } 80 | 81 | Mono countUserProducts(Long userId) { 82 | return db.execute().sql("Select count(id) as total from product where user_id = $1").bind(0, userId).map((row, rowMetadata) -> row.get("total", Long.class)).one(); 83 | } 84 | 85 | Mono countAvailableProducts() { 86 | return db.execute().sql("Select count(id) as total from product where inventory_count > 0").map((row, rowMetadata) -> row.get("total", Long.class)).one(); 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /storage/migration/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 4.0.0 8 | demo.ecommerce 9 | migration 10 | 1.0 11 | 12 | 13 | 5.2.4 14 | 9.1-901.jdbc4 15 | 1.8 16 | filesystem:src/main/resources/flyway/migrations 17 | 18 | 19 | 20 | 21 | 22 | org.flywaydb 23 | flyway-core 24 | ${flyway.version} 25 | 26 | 27 | postgresql 28 | postgresql 29 | ${postgres.driver.version} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-eclipse-plugin 61 | 2.9 62 | 63 | true 64 | false 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 2.3.2 73 | 74 | ${java.version} 75 | ${java.version} 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-assembly-plugin 83 | 84 | 85 | package 86 | 87 | single 88 | 89 | 90 | 91 | 92 | 93 | 94 | demo.ecommerce.migration.Application 95 | 96 | 97 | 98 | jar-with-dependencies 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /system/authentication/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | demo.ecommerce 8 | authentication 9 | 1.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.1.3.RELEASE 15 | 16 | 17 | 18 | 19 | 2.1.1.RELEASE 20 | 1.8 21 | 1.18.0 22 | UTF-8 23 | 1.8 24 | 1.8 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-starter-netflix-eureka-client 32 | 2.1.0.RELEASE 33 | 34 | 35 | org.springframework.cloud 36 | spring-cloud-config-client 37 | 2.1.4.RELEASE 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-security 46 | 47 | 48 | org.springframework.data 49 | spring-data-jdbc 50 | 51 | 52 | 53 | 54 | org.postgresql 55 | postgresql 56 | 42.4.1 57 | 58 | 59 | 60 | commons-dbcp 61 | commons-dbcp 62 | 1.4 63 | 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | ${lombok.version} 69 | compile 70 | 71 | 72 | 73 | 74 | org.springframework.security.oauth.boot 75 | spring-security-oauth2-autoconfigure 76 | 2.1.3.RELEASE 77 | 78 | 79 | org.springframework.security.oauth 80 | spring-security-oauth2 81 | 2.3.6.RELEASE 82 | 83 | 84 | 85 | org.springframework.security 86 | spring-security-rsa 87 | 1.0.8.RELEASE 88 | 89 | 90 | 91 | org.springframework.security 92 | spring-security-oauth2-jose 93 | 5.1.5.RELEASE 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-maven-plugin 103 | 2.1.3.RELEASE 104 | 105 | demo.ecommerce.auth.Application 106 | ZIP 107 | 108 | 109 | 110 | 111 | repackage 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecommerce Microservices Architecture 2 | 3 | **A complete ecommerce demo 4 | Implement based on [Microservice Architecture Pattern](http://martinfowler.com/microservices/) using Spring Boot, Spring Cloud, Spring WebFlux, Postgresdb and Docker.** 5 | 6 | 7 | ## Abstract 8 | In this project I didn't built independent microservices But i implemented microservices share the same database Why? 9 | Because I want to use database tools like `ACID transactions, joins, bulk files imports, ...` 10 | instead of network overhead for interprocess communication between microservices. 11 | I worked in a large project implemented based on microservices architecture we have a set of independent services and 12 | I used [Spring Cloud Open Feign](https://spring.io/projects/spring-cloud-openfeign) 13 | for inter-process communication (Which uses client side load balancer ribbon) 14 | 15 | 16 | I'm planning to implement another version from project with independent microservices and communicate via feign. 17 | 18 | I have split services into two types system and logical services. 19 | 20 | ## Features 21 | 22 | Secured, Authorized and Paginated endpoints. 23 | 24 | ## Functional services 25 | 26 | ### Authentication Service 27 | Authorization Server for all other services which grants [OAuth2 tokens](https://tools.ietf.org/html/rfc6749) for the backend resource services. 28 | All other secured services must set jwk uri for endpoint implemented on this service. 29 | ```spring: 30 | security: 31 | oauth2: 32 | resourceserver: 33 | jwt: 34 | jwk-set-uri: ${JWKS_URL} 35 | ``` 36 | Endpoints 37 | 38 | Method | Path | Description | User authenticated 39 | ------------- | ------------------------- | ------------- |:-------------:| 40 | GET | /.well-known/jwks | System endpoint to get JSON Web Key Set (JWKS) is a set of keys containing the public keys that should be used to verify JWT token | 41 | POST | /user/merchant | Register new merchant account (with merchant role) | 42 | POST | /user/client | Register new client account (with client role)| | 43 | GET | /user | Get current login user information | × 44 | 45 | ### Product Service 46 | Manage products information and inventory 47 | Endpoints 48 | 49 | Method | Path | Description | User authenticated | Role 50 | ------------- | ------------------------- | ------------- |:-------------:| :-------------:| 51 | GET | /list?page={page}&pageSize={pageSize} | Get all products | x | any | 52 | GET | /list/merchant?page={page}&pageSize={pageSize} | Get products created by merchant | x | merchant,admin | 53 | POST| /save | Create new product| x | merchant | 54 | PUT | /save | Update existing product| x | merchant | 55 | GET | /list/available?page={page}&pageSize={pageSize} | Get available products with inventory > 0 | × | any 56 | GET | /list/not-available?page={page}&pageSize={pageSize} | Get available products with inventory = 0 | × | any 57 | 58 | ### Order Service 59 | Manage products information and inventory 60 | Endpoints 61 | 62 | Method | Path | Description | User authenticated | Role 63 | ------------- | ------------------------- | ------------- |:-------------:| :-------------:| 64 | GET | /list/user?page={page}&pageSize={pageSize} | Get login user orders | x | any | 65 | GET | /{orderId} | Get order by id | x | any | 66 | POST| /save | Create new order| x | client | 67 | PUT | /save | Update existing order| x | client | 68 | 69 | 70 | ## System Services 71 | Core service to implement Netflix OSS design architecture and Services token security 72 | 73 | ### Config Service 74 | Provides configuration for all other services (centralize configuration for all services).
75 | Details: [Spring Cloud Config](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html) 76 | 77 | ### Discovery Service 78 | It allows automatic detection of network locations for service instances, which could have dynamically assigned addresses because of auto-scaling, 79 | failures and upgrades (Every service register in this service after running).
80 | Details: [Spring Cloud Netflix](https://spring.io/projects/spring-cloud-netflix)
81 | Eureka server url: http://localhost:2222/ 82 | 83 | 84 | ### Gateway Service 85 | 86 | Provide a proxy (routing) and client side load balancing via ribbon 87 | You can deploy multiple services for the same service and gateway will load balancing between them ( Simple scalability ) 88 | Details: [Spring Cloud Gateway](https://spring.io/projects/spring-cloud-gateway) 89 | 90 | ### Migration Service 91 | Migrations service handle changes on database tables using [flyway](https://flywaydb.org/)
92 | To apply change on database please create file in this folder `ecommerce-microservices/storage/migration/src/main/resources/flyway/migrations` 93 | in this structure `*__*.sql` then apply this command 94 | 95 | ``` 96 | cd ecommerce-microservices/storage/migration # change it to your path 97 | mvn clean package 98 | docker-compose build 99 | docker-compose up migration 100 | ``` 101 | 102 | ### Monitoring 103 | Not completed require implement turbine service and any broker service (rabbitmq) But you can access 104 | start page from http://localhost:8000/hystrix 105 | 106 | # Run Project 107 | Install [maven](https://maven.apache.org/) and [docker](https://docs.docker.com/compose/)
108 | ``` 109 | mvn clean install 110 | docker-compse build 111 | docker-compse up 112 | 113 | # on the first run or after update sql migration on migration service 114 | docker-compose up migration 115 | ``` 116 | 117 | Probably you got issues because docker caching refresh docker images by: 118 | ``` 119 | docker rm $(docker ps -a -q) -f 120 | docker volume prune 121 | ``` 122 | 123 | # Endpoints Documentations 124 | [Endpoints Docs](/endpoints.md) 125 | 126 | 127 | ## Contributions are welcome! 128 | greatly appreciate your help. Feel free to suggest and implement improvements. 129 | 130 | 131 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | demo.ecommerce 8 | ecommerce-microservices 9 | 1.0 10 | pom 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 2.1.3.RELEASE 16 | 17 | 18 | 19 | 20 | 1.8 21 | Greenwich.RELEASE 22 | 2.1.1.RELEASE 23 | 2.0.4.RELEASE 24 | 1.18.0 25 | UTF-8 26 | 1.8 27 | 1.8 28 | 29 | 30 | 31 | services/product 32 | services/order 33 | system/config 34 | system/authentication 35 | system/discovery-server 36 | system/gateway 37 | system/monitoring 38 | components/framework 39 | components/model 40 | storage/migration 41 | 42 | 43 | 44 | 45 | spring-milestones 46 | Spring Milestones 47 | https://repo.spring.io/milestone 48 | 49 | false 50 | 51 | 52 | 53 | spring-snapshots 54 | Spring Snapshots 55 | https://repo.spring.io/snapshot 56 | 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-dependencies 67 | ${spring-cloud.version} 68 | pom 69 | import 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-webflux 79 | ${spring-boot.version} 80 | 81 | 82 | 83 | org.springframework.cloud 84 | spring-cloud-starter-netflix-eureka-client 85 | 86 | 87 | 88 | org.springframework.cloud 89 | spring-cloud-config-client 90 | 91 | 92 | 93 | org.springframework.cloud 94 | spring-cloud-starter 95 | 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-starter-actuator 100 | 101 | 102 | 103 | org.springframework.cloud 104 | spring-cloud-starter-netflix-hystrix 105 | 106 | 107 | 108 | org.springframework.data 109 | spring-data-r2dbc 110 | 1.0.0.M1 111 | 112 | 113 | 114 | io.r2dbc 115 | r2dbc-spi 116 | 1.0.0.M7 117 | 118 | 119 | 120 | io.r2dbc 121 | r2dbc-postgresql 122 | 1.0.0.M7 123 | 124 | 125 | 126 | org.projectlombok 127 | lombok 128 | ${lombok.version} 129 | compile 130 | 131 | 132 | 133 | 134 | org.springframework.security 135 | spring-security-config 136 | 137 | 138 | org.springframework.security 139 | spring-security-oauth2-jose 140 | 141 | 142 | 143 | org.springframework.security 144 | spring-security-oauth2-client 145 | 146 | 147 | org.springframework.security 148 | spring-security-oauth2-resource-server 149 | 150 | 151 | 152 | 153 | 154 | org.springframework.boot 155 | spring-boot-starter-test 156 | test 157 | ${spring-boot-test.version} 158 | 159 | 160 | 161 | io.r2dbc 162 | r2dbc-h2 163 | test 164 | 1.0.0.M7 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /storage/migration/src/main/resources/flyway/migrations/m20190805__Base_Tables.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 10.7 (Ubuntu 10.7-0ubuntu0.18.04.1) 6 | -- Dumped by pg_dump version 11.2 (Ubuntu 11.2-1.pgdg18.04+1) 7 | 8 | -- Started on 2019-07-29 04:35:46 EET 9 | 10 | SET statement_timeout = 0; 11 | SET lock_timeout = 0; 12 | SET idle_in_transaction_session_timeout = 0; 13 | SET client_encoding = 'UTF8'; 14 | SET standard_conforming_strings = on; 15 | SELECT pg_catalog.set_config('search_path', '', false); 16 | SET check_function_bodies = false; 17 | SET client_min_messages = warning; 18 | SET row_security = off; 19 | 20 | -- 21 | -- TOC entry 4 (class 2615 OID 24710) 22 | -- Name: ecommerce; Type: SCHEMA; Schema: -; Owner: ecommerce 23 | -- 24 | -- 25 | -- CREATE SCHEMA ecommerce; 26 | -- 27 | -- 28 | -- ALTER SCHEMA ecommerce OWNER TO ecommerce; 29 | 30 | SET default_tablespace = ''; 31 | 32 | SET default_with_oids = false; 33 | 34 | -- 35 | -- TOC entry 198 (class 1259 OID 16389) 36 | -- Name: product_id_seq; Type: SEQUENCE; Schema: public; Owner: ecommerce 37 | -- 38 | 39 | CREATE SEQUENCE ecommerce.product_id_seq 40 | START WITH 1 41 | INCREMENT BY 1 42 | NO MINVALUE 43 | NO MAXVALUE 44 | CACHE 1; 45 | 46 | 47 | ALTER TABLE ecommerce.product_id_seq OWNER TO ecommerce; 48 | 49 | -- 50 | -- TOC entry 197 (class 1259 OID 16386) 51 | -- Name: product; Type: TABLE; Schema: public; Owner: ecommerce 52 | -- 53 | 54 | CREATE TABLE ecommerce.product ( 55 | id bigint DEFAULT nextval('ecommerce.product_id_seq'::regclass) NOT NULL, 56 | title character varying(250), 57 | sku character varying(150), 58 | inventory_count integer 59 | ); 60 | 61 | 62 | ALTER TABLE ecommerce.product OWNER TO ecommerce; 63 | 64 | -- 65 | -- TOC entry 199 (class 1259 OID 24579) 66 | -- Name: shopping_cart_id_seq; Type: SEQUENCE; Schema: public; Owner: ecommerce 67 | -- 68 | 69 | CREATE SEQUENCE ecommerce.shopping_cart_id_seq 70 | START WITH 1 71 | INCREMENT BY 1 72 | NO MINVALUE 73 | NO MAXVALUE 74 | CACHE 1; 75 | 76 | 77 | ALTER TABLE ecommerce.shopping_cart_id_seq OWNER TO ecommerce; 78 | 79 | -- 80 | -- TOC entry 201 (class 1259 OID 24583) 81 | -- Name: shopping_cart; Type: TABLE; Schema: public; Owner: ecommerce 82 | -- 83 | 84 | CREATE TABLE ecommerce.shopping_cart ( 85 | id bigint DEFAULT nextval('ecommerce.shopping_cart_id_seq'::regclass) NOT NULL, 86 | total_quantity integer, 87 | sub_total_price numeric(18,5), 88 | total_shipping_cost numeric(18,0), 89 | total_cost numeric, 90 | user_id bigint 91 | ); 92 | 93 | 94 | ALTER TABLE ecommerce.shopping_cart OWNER TO ecommerce; 95 | 96 | -- 97 | -- TOC entry 200 (class 1259 OID 24581) 98 | -- Name: shopping_cart_item_id_seq; Type: SEQUENCE; Schema: public; Owner: ecommerce 99 | -- 100 | 101 | CREATE SEQUENCE ecommerce.shopping_cart_item_id_seq 102 | START WITH 1 103 | INCREMENT BY 1 104 | NO MINVALUE 105 | NO MAXVALUE 106 | CACHE 1; 107 | 108 | 109 | ALTER TABLE ecommerce.shopping_cart_item_id_seq OWNER TO ecommerce; 110 | 111 | -- 112 | -- TOC entry 202 (class 1259 OID 24590) 113 | -- Name: shopping_cart_item; Type: TABLE; Schema: public; Owner: ecommerce 114 | -- 115 | 116 | CREATE TABLE ecommerce.shopping_cart_item ( 117 | id bigint DEFAULT nextval('ecommerce.shopping_cart_item_id_seq'::regclass) NOT NULL, 118 | quantity integer, 119 | unit_price numeric, 120 | total_price numeric, 121 | shipping_cost numeric, 122 | product_id bigint, 123 | shopping_cart_id bigint 124 | ); 125 | 126 | 127 | ALTER TABLE ecommerce.shopping_cart_item OWNER TO ecommerce; 128 | 129 | -- 130 | -- TOC entry 204 (class 1259 OID 24671) 131 | -- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: ecommerce 132 | -- 133 | 134 | CREATE SEQUENCE ecommerce.user_id_seq 135 | START WITH 0 136 | INCREMENT BY 1 137 | MINVALUE 0 138 | NO MAXVALUE 139 | CACHE 1; 140 | 141 | 142 | ALTER TABLE ecommerce.user_id_seq OWNER TO ecommerce; 143 | 144 | -- 145 | -- TOC entry 203 (class 1259 OID 24645) 146 | -- Name: users; Type: TABLE; Schema: public; Owner: ecommerce 147 | -- 148 | 149 | CREATE TABLE ecommerce.users ( 150 | id bigint DEFAULT nextval('ecommerce.user_id_seq'::regclass) NOT NULL, 151 | first_name character varying(100), 152 | last_name character varying(100), 153 | create_date date, 154 | update_date date, 155 | last_login date, 156 | email character varying(100), 157 | password text 158 | ); 159 | 160 | 161 | ALTER TABLE ecommerce.users OWNER TO ecommerce; 162 | 163 | -- 164 | -- TOC entry 2969 (class 0 OID 0) 165 | -- Dependencies: 198 166 | -- Name: product_id_seq; Type: SEQUENCE SET; Schema: public; Owner: ecommerce 167 | -- 168 | 169 | SELECT pg_catalog.setval('ecommerce.product_id_seq', 3, true); 170 | 171 | 172 | -- 173 | -- TOC entry 2970 (class 0 OID 0) 174 | -- Dependencies: 199 175 | -- Name: shopping_cart_id_seq; Type: SEQUENCE SET; Schema: public; Owner: ecommerce 176 | -- 177 | 178 | SELECT pg_catalog.setval('ecommerce.shopping_cart_id_seq', 24, true); 179 | 180 | 181 | -- 182 | -- TOC entry 2971 (class 0 OID 0) 183 | -- Dependencies: 200 184 | -- Name: shopping_cart_item_id_seq; Type: SEQUENCE SET; Schema: public; Owner: ecommerce 185 | -- 186 | 187 | SELECT pg_catalog.setval('ecommerce.shopping_cart_item_id_seq', 11, true); 188 | 189 | 190 | -- 191 | -- TOC entry 2972 (class 0 OID 0) 192 | -- Dependencies: 204 193 | -- Name: user_id_seq; Type: SEQUENCE SET; Schema: public; Owner: ecommerce 194 | -- 195 | 196 | SELECT pg_catalog.setval('ecommerce.user_id_seq', 2, true); 197 | 198 | 199 | -- 200 | -- TOC entry 2823 (class 2606 OID 24598) 201 | -- Name: shopping_cart_item cart_item_pk; Type: CONSTRAINT; Schema: public; Owner: ecommerce 202 | -- 203 | 204 | ALTER TABLE ONLY ecommerce.shopping_cart_item 205 | ADD CONSTRAINT cart_item_pk PRIMARY KEY (id); 206 | 207 | 208 | -- 209 | -- TOC entry 2821 (class 2606 OID 24606) 210 | -- Name: shopping_cart cart_pk; Type: CONSTRAINT; Schema: public; Owner: ecommerce 211 | -- 212 | 213 | ALTER TABLE ONLY ecommerce.shopping_cart 214 | ADD CONSTRAINT cart_pk PRIMARY KEY (id); 215 | 216 | 217 | -- 218 | -- TOC entry 2819 (class 2606 OID 16392) 219 | -- Name: product product_pk; Type: CONSTRAINT; Schema: public; Owner: ecommerce 220 | -- 221 | 222 | ALTER TABLE ONLY ecommerce.product 223 | ADD CONSTRAINT product_pk PRIMARY KEY (id); 224 | 225 | 226 | -- 227 | -- TOC entry 2827 (class 2606 OID 24649) 228 | -- Name: users user_pk; Type: CONSTRAINT; Schema: public; Owner: ecommerce 229 | -- 230 | 231 | ALTER TABLE ONLY ecommerce.users 232 | ADD CONSTRAINT user_pk PRIMARY KEY (id); 233 | 234 | 235 | -- 236 | -- TOC entry 2824 (class 1259 OID 24617) 237 | -- Name: fki_cart_item_cart_fk; Type: INDEX; Schema: public; Owner: ecommerce 238 | -- 239 | 240 | CREATE INDEX fki_cart_item_cart_fk ON ecommerce.shopping_cart_item USING btree (shopping_cart_id); 241 | 242 | 243 | -- 244 | -- TOC entry 2825 (class 1259 OID 24604) 245 | -- Name: fki_cart_item_product_fk; Type: INDEX; Schema: public; Owner: ecommerce 246 | -- 247 | 248 | CREATE INDEX fki_cart_item_product_fk ON ecommerce.shopping_cart_item USING btree (product_id); 249 | 250 | 251 | -- 252 | -- TOC entry 2833 (class 2606 OID 24612) 253 | -- Name: shopping_cart_item cart_item_cart_fk; Type: FK CONSTRAINT; Schema: public; Owner: ecommerce 254 | -- 255 | 256 | ALTER TABLE ONLY ecommerce.shopping_cart_item 257 | ADD CONSTRAINT cart_item_cart_fk FOREIGN KEY (shopping_cart_id) REFERENCES ecommerce.shopping_cart(id); 258 | 259 | 260 | -- 261 | -- TOC entry 2832 (class 2606 OID 24599) 262 | -- Name: shopping_cart_item cart_item_product_fk; Type: FK CONSTRAINT; Schema: public; Owner: ecommerce 263 | -- 264 | 265 | ALTER TABLE ONLY ecommerce.shopping_cart_item 266 | ADD CONSTRAINT cart_item_product_fk FOREIGN KEY (product_id) REFERENCES ecommerce.product(id); 267 | 268 | 269 | -- 270 | -- TOC entry 2831 (class 2606 OID 24650) 271 | -- Name: shopping_cart user_fk; Type: FK CONSTRAINT; Schema: public; Owner: ecommerce 272 | -- 273 | 274 | ALTER TABLE ONLY ecommerce.shopping_cart 275 | ADD CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES ecommerce.users(id); 276 | 277 | 278 | -- Completed on 2019-07-29 04:35:50 EET 279 | 280 | -- 281 | -- PostgreSQL database dump complete 282 | -- 283 | 284 | -------------------------------------------------------------------------------- /services/order/src/main/java/demo/ecommerce/order/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package demo.ecommerce.order.service; 2 | 3 | import demo.ecommerce.model.order.ShoppingCart; 4 | import demo.ecommerce.model.order.ShoppingCartItem; 5 | import demo.ecommerce.model.product.Product; 6 | import demo.ecommerce.model.user.User; 7 | import demo.ecommerce.repository.order.ShoppingCartItemRepository; 8 | import demo.ecommerce.repository.order.ShoppingCartRepository; 9 | import demo.ecommerce.repository.product.ProductRepository; 10 | import demo.ecommerce.repository.user.UserRepository; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageImpl; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.data.r2dbc.function.DatabaseClient; 18 | import org.springframework.stereotype.Service; 19 | import reactor.core.publisher.Flux; 20 | import reactor.core.publisher.Mono; 21 | 22 | import java.math.BigDecimal; 23 | import java.util.*; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.function.BiFunction; 26 | import java.util.function.Function; 27 | import java.util.stream.Collectors; 28 | import java.util.stream.Stream; 29 | 30 | 31 | /** 32 | * @Author Mostafa Albana 33 | */ 34 | 35 | @Service 36 | public class OrderService { 37 | 38 | private static String PRODUCT_NOT_AVAILABLE_MESSAGE = "Product %s is not Available"; 39 | private static Logger logger = LoggerFactory.getLogger(OrderService.class); 40 | 41 | @Autowired 42 | ShoppingCartItemRepository shoppingCartItemRepository; 43 | 44 | @Autowired 45 | ShoppingCartRepository shoppingCartRepository; 46 | 47 | @Autowired 48 | ProductRepository productRepository; 49 | 50 | @Autowired 51 | UserRepository userRepository; 52 | 53 | @Autowired 54 | DatabaseClient db; 55 | 56 | 57 | Mono updateCartData(ShoppingCart cart, Mono existingCart, Flux productFlux) { 58 | if (existingCart != null) { 59 | // delete items removed by user 60 | existingCart.flatMap(ecart -> { 61 | HashSet newItemsId = new HashSet(); 62 | cart.getCartItemList().forEach(i -> { 63 | if (i.getId() != null) newItemsId.add(i.getId()); 64 | }); 65 | //existingItems.stream().filter(i -> ) 66 | List deletedItems = ecart.getCartItemList().stream().filter(i -> !newItemsId.contains(i.getId())).collect(Collectors.toList()); 67 | if (deletedItems.size() > 0) deleteCartItems(deletedItems).subscribe(); 68 | return Mono.just(ecart); 69 | }).subscribe(); 70 | } 71 | return productFlux.collectList().flatMap(products -> { 72 | BigDecimal totalCost = BigDecimal.ZERO; 73 | BigDecimal totalShipping = BigDecimal.ZERO; 74 | int totalQuantity = 0; 75 | 76 | for (int i = 0; i < cart.getCartItemList().size(); i++) { 77 | ShoppingCartItem item = cart.getCartItemList().get(i); 78 | 79 | // set price data from product 80 | item.setUnitPrice(products.get(i).getSellPrice()); 81 | item.setUnitCostPrice(products.get(i).getCostPrice()); 82 | 83 | BigDecimal itemTotal = BigDecimal.valueOf(item.getQuantity()).multiply(BigDecimal.valueOf(item.getUnitPrice())); 84 | item.setTotalPrice(itemTotal.doubleValue()); 85 | 86 | totalCost = totalCost.add(itemTotal); 87 | totalQuantity += item.getQuantity(); 88 | totalShipping = totalShipping.add(BigDecimal.valueOf(item.getShippingCost())); 89 | } 90 | 91 | cart.setSubTotalPrice(totalCost.doubleValue()); 92 | cart.setTotalShippingCost(totalShipping.doubleValue()); 93 | // total + shipping 94 | cart.setTotalCost(totalCost.add(totalShipping).doubleValue()); 95 | cart.setTotalQuantity(totalQuantity); 96 | 97 | return Mono.just(cart); 98 | }); 99 | } 100 | 101 | private Mono validateQuantities(ShoppingCart cart, Mono existingCart, BiFunction, Mono> onSuccess, Function> onFail) { 102 | 103 | // get all products ids from cart items 104 | List productIds = cart.getCartItemList().stream().map(itm -> itm.getProduct().getId()).collect(Collectors.toList()); 105 | final Flux newCartProducts = productRepository.findAllById(productIds); 106 | 107 | 108 | Flux products; 109 | if (existingCart != null) { 110 | 111 | products = existingCart.flatMapMany(oldCart -> { 112 | Map oldCartQuantities = oldCart.getCartItemList().stream().collect(Collectors.toMap(ShoppingCartItem::getProductId, ShoppingCartItem::getQuantity)); 113 | return newCartProducts.flatMap(product -> { 114 | if (oldCartQuantities.containsKey(product.getId())) { 115 | product.increaseInventory(oldCartQuantities.get(product.getId())); 116 | } 117 | return Mono.just(product); 118 | }); 119 | }); 120 | } else { 121 | products = newCartProducts; 122 | } 123 | AtomicInteger index = new AtomicInteger(0); 124 | StringBuilder errorMessageBuilder = new StringBuilder(); 125 | Flux validProducts = products.filter(product -> { 126 | int cartIndex = index.getAndIncrement(); 127 | if (product.getInventoryCounts() >= cart.getCartItemList().get(cartIndex).getQuantity()) { 128 | return true; 129 | } else { 130 | if (errorMessageBuilder.length() > 0) errorMessageBuilder.append(", "); 131 | errorMessageBuilder.append(String.format(PRODUCT_NOT_AVAILABLE_MESSAGE, product.getSku())); 132 | return false; 133 | } 134 | }); 135 | 136 | return validProducts.count().flatMap(validCount -> 137 | products.count().flatMap(allproductsCount -> { 138 | if (validCount.equals(allproductsCount)) { 139 | return onSuccess.apply(cart, existingCart); 140 | } else { 141 | 142 | return onFail.apply(errorMessageBuilder.toString()); 143 | } 144 | }) 145 | ); 146 | } 147 | 148 | private Mono> restoreProductsQuantities(ShoppingCart oldCart) { 149 | 150 | List productIds = oldCart.getCartItemList().stream().map(ShoppingCartItem::getProductId).collect(Collectors.toList()); 151 | Flux products = productRepository.findAllById(productIds); 152 | AtomicInteger indexer = new AtomicInteger(0); 153 | final Map productMap = new HashMap<>(); 154 | return products.collectList().flatMap(productList -> { 155 | for (Product product : productList) { 156 | product.increaseInventory(oldCart.getCartItemList().get(indexer.getAndIncrement()).getQuantity()); 157 | productMap.put(product.getId(), product); 158 | } 159 | return Mono.just(productMap); 160 | }); 161 | 162 | } 163 | 164 | private Flux updateCartProductQuantities(ShoppingCart cart, Mono existingCart) { 165 | final Mono> oldProductsRestored; 166 | 167 | // if edit CartOrder increase counts of products from old cart 168 | if (existingCart != null) { 169 | oldProductsRestored = existingCart.flatMap(this::restoreProductsQuantities); 170 | } else { 171 | oldProductsRestored = Mono.just(new HashMap<>()); 172 | } 173 | 174 | List productIds = cart.getCartItemList().stream().map(item -> item.getProduct().getId()).collect(Collectors.toList()); 175 | Flux newCartProducts = productRepository.findAllById(productIds); 176 | 177 | 178 | return newCartProducts.collectList().flatMapMany(newProducts -> { 179 | Map newProductMap = new HashMap<>(); 180 | newProducts.forEach(np -> newProductMap.put(np.getId(), np)); 181 | 182 | return oldProductsRestored.flatMapMany(oldProductsMap -> { 183 | 184 | List productsToSave = new ArrayList<>(); 185 | 186 | for (int i = 0; i < newProducts.size(); i++) { 187 | if (oldProductsMap.containsKey(newProducts.get(i).getId())) { 188 | Product product = oldProductsMap.get(newProducts.get(i).getId()); 189 | product.decreaseInventory(cart.getCartItemList().get(i).getQuantity()); 190 | productsToSave.add(product); 191 | } else { 192 | newProducts.get(i).decreaseInventory(cart.getCartItemList().get(i).getQuantity()); 193 | productsToSave.add(newProducts.get(i)); 194 | } 195 | } 196 | 197 | 198 | // find delete items products to save them 199 | List deletedProducts = oldProductsMap.keySet().stream() 200 | .filter(opk -> !newProductMap.containsKey(opk)) 201 | .flatMap(deletedProductIds -> Stream.of(oldProductsMap.get(deletedProductIds))).collect(Collectors.toList()); 202 | productsToSave.addAll(deletedProducts); 203 | 204 | return Flux.fromArray(productsToSave.toArray(new Product[0])); 205 | 206 | }); 207 | 208 | }); 209 | 210 | } 211 | 212 | 213 | public Mono saveShoppingCart(ShoppingCart cart, String email) { 214 | return userRepository.getUserByEmail(email).flatMap(user -> { 215 | Mono existingCartMono = null; 216 | cart.setUserId(user.getId()); 217 | if (cart.getId() == null) { 218 | cart.setCreatedOn(new Date()); 219 | } else { 220 | existingCartMono = shoppingCartRepository.findById(cart.getId()).flatMap(this::fillCartWithCartItems); 221 | } 222 | cart.setUpdatedOn(new Date()); 223 | 224 | // validate client not save another cart 225 | final Mono tempExistingCart = existingCartMono; 226 | return validateUserId(cart, existingCartMono, user).flatMap(vc -> 227 | validateQuantities(cart, tempExistingCart, this::internalSaveShoppingCart, errorMessage -> { 228 | throw new IllegalArgumentException(errorMessage); 229 | }) 230 | ); 231 | 232 | 233 | }); 234 | 235 | } 236 | 237 | Mono validateUserId(ShoppingCart cart, Mono cartMono, User user) { 238 | 239 | if (cartMono != null) { 240 | return validateUserId(cartMono, user); 241 | } 242 | return Mono.just(cart); 243 | } 244 | 245 | Mono validateUserId(Mono cart, User user) { 246 | return cart.flatMap(dbCart -> { 247 | if (!dbCart.getUserId().equals(user.getId())) { 248 | throw new IllegalArgumentException("Not authorized to save this order"); 249 | } 250 | return cart; 251 | }); 252 | } 253 | 254 | // You should never call a blocking method within a method that returns a reactive type 255 | private Mono internalSaveShoppingCart(ShoppingCart shoppingCart, Mono existingCartMono) { 256 | 257 | // update product Quantities and save into database 258 | Flux savedProducts = productRepository.saveAll(updateCartProductQuantities(shoppingCart, existingCartMono)); 259 | savedProducts.subscribe(products -> logger.info("Update product quantities")); 260 | 261 | Mono savedShoppingCart = updateCartData(shoppingCart, existingCartMono, savedProducts).flatMap(cartWithData -> shoppingCartRepository.save(cartWithData)); 262 | 263 | 264 | // assign saveShoppingCart is required to fill with data enhanced from calling flatMap 265 | savedShoppingCart = savedShoppingCart.flatMap(savedCart -> { 266 | 267 | shoppingCart.getCartItemList().forEach(cartItem -> { 268 | 269 | cartItem.setShoppingCartId(savedCart.getId()); 270 | cartItem.setProductId(cartItem.getProduct().getId()); 271 | }); 272 | 273 | 274 | // calling subscribe/flatMap/... is a must to actually save in database otherwise data will not saved to database. 275 | Flux cartItemFlux = shoppingCartItemRepository.saveAll(shoppingCart.getCartItemList()).flatMap(this::fillCartItemWithProduct); 276 | 277 | return cartItemFlux.collectList().flatMap(a -> { 278 | savedCart.setCartItemList(a); 279 | return Mono.just(savedCart); 280 | }); 281 | 282 | }); 283 | 284 | return savedShoppingCart; 285 | } 286 | 287 | public Mono deleteCartItems(List cartItems) { 288 | return shoppingCartItemRepository.deleteAll(cartItems); 289 | } 290 | 291 | public Mono getShoppingCart(Long cartId, String email) { 292 | 293 | final Mono shoppingCartMono = shoppingCartRepository.findById(cartId); 294 | 295 | return userRepository.getUserByEmail(email).flatMap(user -> 296 | 297 | validateUserId(shoppingCartMono, user).flatMap(valid -> 298 | shoppingCartMono.flatMap(this::fillCartWithCartItems) 299 | ) 300 | ); 301 | 302 | } 303 | 304 | 305 | public Mono> getUserShoppingCarts(String email, Pageable pageable) { 306 | return userRepository.getUserByEmail(email).flatMap(user -> { 307 | Flux shoppingCarts = shoppingCartRepository.getUserShoppingCartsPageable(user.getId(), pageable.getPageSize(), pageable.getOffset()); 308 | shoppingCarts = shoppingCarts.flatMap(this::fillCartWithCartItems); 309 | Mono totalCartsCount = countUserShoppingCarts(user.getId()); 310 | return shoppingCarts.collectList().flatMap(carts -> 311 | totalCartsCount.flatMap(count -> Mono.just(new PageImpl(carts, pageable, count)))); 312 | }); 313 | } 314 | 315 | 316 | Mono fillCartWithCartItems(ShoppingCart cart) { 317 | Flux shoppingCartItemFlux = shoppingCartItemRepository.getShoppingOrderCartItems(cart.getId()); 318 | Flux result = shoppingCartItemFlux.flatMap(this::fillCartItemWithProduct); 319 | 320 | return result.collectList().flatMap(a -> { 321 | cart.setCartItemList(a); 322 | return Mono.just(cart); 323 | }); 324 | } 325 | 326 | Mono fillCartItemWithProduct(ShoppingCartItem cartItem) { 327 | 328 | if (cartItem.getProductId() != null) { 329 | return productRepository.findById(cartItem.getProductId()).flatMap(product -> { 330 | // hide cost price details from end-user client (available only for merchants 331 | product.setCostPrice(null); 332 | cartItem.setProduct(product); 333 | return Mono.just(cartItem); 334 | }); 335 | } else { 336 | return Mono.just(cartItem); 337 | } 338 | } 339 | 340 | Mono countUserShoppingCarts(Long userId) { 341 | return db.execute().sql("Select count(id) as total from shopping_cart where user_id = $1").bind(0, userId).map((row, rowMetadata) -> row.get("total", Long.class)).one(); 342 | } 343 | 344 | } 345 | --------------------------------------------------------------------------------