diff --git a/bill/build.gradle b/bill/build.gradle index 473c635..8cbe5ce 100644 --- a/bill/build.gradle +++ b/bill/build.gradle @@ -13,5 +13,13 @@ dependencies { implementation project(':common') annotationProcessor project(':common') +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Open Api UI dependency implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' } diff --git a/bill/src/main/java/com/tungstun/bill/BillApplication.java b/bill/src/main/java/com/tungstun/bill/BillApplication.java index 716c5ec..0bbd149 100644 --- a/bill/src/main/java/com/tungstun/bill/BillApplication.java +++ b/bill/src/main/java/com/tungstun/bill/BillApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.kafka.annotation.EnableKafka; @SpringBootApplication @ComponentScan("com.tungstun") @EnableKafka +@EnableEurekaClient public class BillApplication { public static void main(String[] args) { SpringApplication.run(BillApplication.class, args); diff --git a/bill/src/main/java/com/tungstun/bill/config/SwaggerConfig.java b/bill/src/main/java/com/tungstun/bill/config/SwaggerConfig.java index 9b4da2e..a1194ba 100644 --- a/bill/src/main/java/com/tungstun/bill/config/SwaggerConfig.java +++ b/bill/src/main/java/com/tungstun/bill/config/SwaggerConfig.java @@ -1,27 +1,46 @@ package com.tungstun.bill.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.models.annotations.OpenAPI31; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@OpenAPI31 -@OpenAPIDefinition( - info = @Info( - title = "bartap Backend Api - Bill", - description = "Bill API of the bartap Backend API microservice cluster containing bill functionality", - version = "1.0", - contact = @Contact( - name = "Tungstun", - url = "https://github.com/tungstun-ict", - email = "jort@tungstun.nl" - ) - ), - tags = { - @Tag(name = "Bill", description = "Functionality based around the Bill"), - @Tag(name = "Order", description = "Functionality based around the Order and Order History"), - } -) +@Configuration public class SwaggerConfig { + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Bill") + .description("Bill API of the bartap Backend API microservice cluster containing bill functionality") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .addTagsItem(new Tag().name("Bill").description("Functionality based around the Bill")) + .addTagsItem(new Tag().name("Order").description("Functionality based around the Order")) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null) { + openApi.addServersItem(new Server() + .description("Gateway Url") + .url(gatewayUrl) + ); + } + + return openApi; + } } diff --git a/bill/src/main/resources/application-dev.properties b/bill/src/main/resources/application-dev.properties index de1262c..88f3129 100644 --- a/bill/src/main/resources/application-dev.properties +++ b/bill/src/main/resources/application-dev.properties @@ -1,7 +1,8 @@ #JWT variables JWT_SECRET=lets-make-a-bar-application-and-use-this-as-a-session-token-secret JWT_ISSUER=bartap-security-service - #Database properties and variables JDBC_DATABASE_URL=jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 DRIVER_CLASS_NAME=org.h2.Driver +#Gateway url +GATEWAY_URL=http://localhost:8081${server.servlet.context-path} diff --git a/bill/src/main/resources/application.properties b/bill/src/main/resources/application.properties index a19c589..4a8a05b 100644 --- a/bill/src/main/resources/application.properties +++ b/bill/src/main/resources/application.properties @@ -1,5 +1,5 @@ # Standard configurations -spring.application.name=bartap-bill +spring.application.name=bill spring.profiles.active=dev server.port=8083 server.servlet.context-path=/api @@ -15,7 +15,10 @@ spring.datasource.driverClassName=${DRIVER_CLASS_NAME} spring.datasource.url=${JDBC_DATABASE_URL} spring.datasource.username=${JDBC_DATABASE_USERNAME} spring.datasource.password=${JDBC_DATABASE_PASSWORD} - # Kafka spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVERS:localhost:9092} spring.kafka.consumer.auto-offset-reset=earliest +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true diff --git a/build.gradle b/build.gradle index 3202235..492fe38 100644 --- a/build.gradle +++ b/build.gradle @@ -26,24 +26,23 @@ subprojects { apply plugin: 'jacoco' dependencies { +// Basic Spring dependencies (used by most) implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.hibernate.validator:hibernate-validator:6.1.5.Final' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + +// In-memory database for easy local spin up and testing implementation 'com.h2database:h2' - implementation 'io.swagger.core.v3:swagger-core:2.2.1' +// Spring Kafka implementation 'org.springframework.kafka:spring-kafka' implementation 'org.springframework.kafka:spring-kafka-test' testImplementation 'org.springframework.kafka:spring-kafka-test' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - testImplementation 'io.projectreactor:reactor-test' } + task prepareKotlinBuildScriptModel {} task wrapper {} diff --git a/common/build.gradle b/common/build.gradle index 2021cf5..5794c22 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -8,13 +8,21 @@ repositories { } dependencies { +// Spring dependencies implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + +// auth0 implementation 'com.auth0:java-jwt:3.19.2' + +// Google dependencies implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49' + +// Reflections library implementation 'org.reflections:reflections:0.10.2' - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } java { diff --git a/common/src/main/java/com/tungstun/common/locale/TimeZoneConfig.java b/common/src/main/java/com/tungstun/common/locale/TimeZoneConfig.java index a971d9d..be9d8e4 100644 --- a/common/src/main/java/com/tungstun/common/locale/TimeZoneConfig.java +++ b/common/src/main/java/com/tungstun/common/locale/TimeZoneConfig.java @@ -20,4 +20,8 @@ private void loadTimezoneConfig() { public void setTimezone(String timezone) { this.timezone = timezone; } + + public String getTimezone() { + return timezone; + } } diff --git a/core/build.gradle b/core/build.gradle index 89c1ad0..bb354f0 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -13,6 +13,14 @@ dependencies { implementation project(':common') annotationProcessor project(':common') +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Open Api UI dependency implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' } diff --git a/core/src/main/java/com/tungstun/core/CoreApplication.java b/core/src/main/java/com/tungstun/core/CoreApplication.java index 3d56d5b..521e2d2 100644 --- a/core/src/main/java/com/tungstun/core/CoreApplication.java +++ b/core/src/main/java/com/tungstun/core/CoreApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.kafka.annotation.EnableKafka; @SpringBootApplication @ComponentScan("com.tungstun") @EnableKafka +@EnableEurekaClient public class CoreApplication { public static void main(String[] args) { SpringApplication.run(CoreApplication.class, args); diff --git a/core/src/main/java/com/tungstun/core/config/SwaggerConfig.java b/core/src/main/java/com/tungstun/core/config/SwaggerConfig.java index bce040c..7ca3bfa 100644 --- a/core/src/main/java/com/tungstun/core/config/SwaggerConfig.java +++ b/core/src/main/java/com/tungstun/core/config/SwaggerConfig.java @@ -1,27 +1,46 @@ package com.tungstun.core.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.models.annotations.OpenAPI31; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@OpenAPI31 -@OpenAPIDefinition( - info = @Info( - title = "bartap Backend Api - Core", - description = "Core API of the bartap Backend API microservice cluster containing bar and session functionality", - version = "1.0", - contact = @Contact( - name = "Tungstun", - url = "https://github.com/tungstun-ict", - email = "jort@tungstun.nl" - ) - ), - tags = { - @Tag(name = "Bar", description = "Functionality based around the Bar"), - @Tag(name = "Session", description = "Functionality based around the Session") - } -) +@Configuration public class SwaggerConfig { + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Core") + .description("Core API of the bartap Backend API microservice cluster containing bar and session functionality") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .addTagsItem(new Tag().name("Bar").description("Functionality based around the Bar")) + .addTagsItem(new Tag().name("Session").description("Functionality based around the Session")) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null) { + openApi.addServersItem(new Server() + .description("Gateway Url") + .url(gatewayUrl) + ); + } + + return openApi; + } } diff --git a/core/src/main/resources/application-dev.properties b/core/src/main/resources/application-dev.properties index de1262c..88f3129 100644 --- a/core/src/main/resources/application-dev.properties +++ b/core/src/main/resources/application-dev.properties @@ -1,7 +1,8 @@ #JWT variables JWT_SECRET=lets-make-a-bar-application-and-use-this-as-a-session-token-secret JWT_ISSUER=bartap-security-service - #Database properties and variables JDBC_DATABASE_URL=jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 DRIVER_CLASS_NAME=org.h2.Driver +#Gateway url +GATEWAY_URL=http://localhost:8081${server.servlet.context-path} diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties index 63b2d54..ba7a1d3 100644 --- a/core/src/main/resources/application.properties +++ b/core/src/main/resources/application.properties @@ -1,5 +1,5 @@ # Standard configurations -spring.application.name=bartap-core +spring.application.name=core spring.profiles.active=dev server.port=8082 server.servlet.context-path=/api @@ -15,7 +15,10 @@ spring.datasource.driverClassName=${DRIVER_CLASS_NAME} spring.datasource.url=${JDBC_DATABASE_URL} spring.datasource.username=${JDBC_DATABASE_USERNAME} spring.datasource.password=${JDBC_DATABASE_PASSWORD} - # Kafka spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVERS:localhost:9092} -spring.kafka.consumer.auto-offset-reset=earliest \ No newline at end of file +spring.kafka.consumer.auto-offset-reset=earliest +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true diff --git a/eureka-server/build.gradle b/eureka-server/build.gradle new file mode 100644 index 0000000..d2e56db --- /dev/null +++ b/eureka-server/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' + id 'application' +} + +//project.mainClassName = 'com.tungstun.eurekaserver.EurekaServerApplication' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server:3.1.3' +} diff --git a/eureka-server/src/main/java/com/tungstun/eurekaserver/EurekaServerApplication.java b/eureka-server/src/main/java/com/tungstun/eurekaserver/EurekaServerApplication.java new file mode 100644 index 0000000..4003325 --- /dev/null +++ b/eureka-server/src/main/java/com/tungstun/eurekaserver/EurekaServerApplication.java @@ -0,0 +1,16 @@ +package com.tungstun.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaServerApplication { + + + public static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } + +} diff --git a/eureka-server/src/main/resources/application.properties b/eureka-server/src/main/resources/application.properties new file mode 100644 index 0000000..ec0c2e3 --- /dev/null +++ b/eureka-server/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=eureka-server +server.port=8761 +# Eureka +eureka.client.fetch-registry=false +eureka.client.register-with-eureka=false diff --git a/eureka-server/src/test/java/com/tungstun/eurekaserver/EurekaServerApplicationTests.java b/eureka-server/src/test/java/com/tungstun/eurekaserver/EurekaServerApplicationTests.java new file mode 100644 index 0000000..0c905e5 --- /dev/null +++ b/eureka-server/src/test/java/com/tungstun/eurekaserver/EurekaServerApplicationTests.java @@ -0,0 +1,13 @@ +package com.tungstun.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/gateway/build.gradle b/gateway/build.gradle index 40f3f25..566d02b 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -19,10 +19,27 @@ dependencies { implementation project(':common') annotationProcessor project(':common') +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Spring Webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' + testImplementation 'io.projectreactor:reactor-test' + +// Spring Cloud dependencies implementation 'org.springframework.cloud:spring-cloud-gateway:3.1.3' implementation 'org.springframework.cloud:spring-cloud-gateway-server:3.1.3' implementation 'org.springframework.cloud:spring-cloud-gateway-webflux:3.1.3' implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Springdoc OpenAPI / Swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.6.9' + implementation 'org.springdoc:springdoc-openapi-security:1.6.9' } dependencyManagement { diff --git a/gateway/src/main/java/com/tungstun/gateway/GatewayApplication.java b/gateway/src/main/java/com/tungstun/gateway/GatewayApplication.java index 7e5b4bd..2a6bb43 100644 --- a/gateway/src/main/java/com/tungstun/gateway/GatewayApplication.java +++ b/gateway/src/main/java/com/tungstun/gateway/GatewayApplication.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableEurekaClient +@EnableScheduling public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); diff --git a/gateway/src/main/java/com/tungstun/gateway/route/BartapRouteConfiguration.java b/gateway/src/main/java/com/tungstun/gateway/route/BartapRouteConfiguration.java index 5af76d8..fa53a98 100644 --- a/gateway/src/main/java/com/tungstun/gateway/route/BartapRouteConfiguration.java +++ b/gateway/src/main/java/com/tungstun/gateway/route/BartapRouteConfiguration.java @@ -17,43 +17,39 @@ public class BartapRouteConfiguration { private static final int REQUEST_TIME_LIMIT = 3; - private final RouteUriConfig routeUriConfig; + private final ServiceRoutes serviceRoutes; - public BartapRouteConfiguration(RouteUriConfig routeUriConfig) { - this.routeUriConfig = routeUriConfig; + public BartapRouteConfiguration(ServiceRoutes serviceRoutes) { + this.serviceRoutes = serviceRoutes; } @Bean public RouteLocator gatewayRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("security", r -> r.path("/api/authentication/**") - .uri(routeUriConfig.getSecurity())) + .uri(serviceRoutes.getSecurity())) .route("user", r -> r.path("/api/user/**") - .uri(routeUriConfig.getSecurity())) + .uri(serviceRoutes.getSecurity())) .route("core", r -> r.path("/api/bars/**") - .uri(routeUriConfig.getCore())) + .uri(serviceRoutes.getCore())) .route("session", r -> r.path("/api/session/**") - .uri(routeUriConfig.getCore())) + .uri(serviceRoutes.getCore())) .route("bill", r -> r.path("/api/bills/**") - .uri(routeUriConfig.getBill())) + .uri(serviceRoutes.getBill())) .route("person", r -> r.path("/api/people/**") - .uri(routeUriConfig.getPerson())) + .uri(serviceRoutes.getPerson())) .route("product", r -> r.path("/api/products/**") - .uri(routeUriConfig.getProduct())) + .uri(serviceRoutes.getProduct())) .route("category", r -> r.path("/api/categories/**") - .uri(routeUriConfig.getProduct())) + .uri(serviceRoutes.getProduct())) .route("order", r -> r.path("/api/order/**") - .uri(routeUriConfig.getOrder())) - - - - + .uri(serviceRoutes.getOrder())) // .filters(filter -> filter // .retry(3) diff --git a/gateway/src/main/java/com/tungstun/gateway/route/RouteUriConfig.java b/gateway/src/main/java/com/tungstun/gateway/route/ServiceRoutes.java similarity index 83% rename from gateway/src/main/java/com/tungstun/gateway/route/RouteUriConfig.java rename to gateway/src/main/java/com/tungstun/gateway/route/ServiceRoutes.java index e145903..de85ecd 100644 --- a/gateway/src/main/java/com/tungstun/gateway/route/RouteUriConfig.java +++ b/gateway/src/main/java/com/tungstun/gateway/route/ServiceRoutes.java @@ -1,13 +1,11 @@ package com.tungstun.gateway.route; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration -@ConfigurationProperties(prefix = "com.tungstun.bartap.routes") -@EnableConfigurationProperties(RouteUriConfig.class) -public class RouteUriConfig { +@ConfigurationProperties(prefix = "com.tungstun.services") +public class ServiceRoutes { private String security; private String core; private String bill; @@ -15,6 +13,7 @@ public class RouteUriConfig { private String product; private String order; + // public String getSecurity() { return security; } @@ -31,20 +30,20 @@ public void setCore(String core) { this.core = core; } - public String getPerson() { - return person; + public String getBill() { + return bill; } - public void setPerson(String person) { - this.person = person; + public void setBill(String bill) { + this.bill = bill; } - public String getOrder() { - return order; + public String getPerson() { + return person; } - public void setOrder(String order) { - this.order = order; + public void setPerson(String person) { + this.person = person; } public String getProduct() { @@ -55,11 +54,12 @@ public void setProduct(String product) { this.product = product; } - public String getBill() { - return bill; + public String getOrder() { + return order; } - public void setBill(String bill) { - this.bill = bill; + public void setOrder(String order) { + this.order = order; } + // } diff --git a/gateway/src/main/java/com/tungstun/gateway/swagger/BartapSwaggerUiConfigParameters.java b/gateway/src/main/java/com/tungstun/gateway/swagger/BartapSwaggerUiConfigParameters.java new file mode 100644 index 0000000..9de31f7 --- /dev/null +++ b/gateway/src/main/java/com/tungstun/gateway/swagger/BartapSwaggerUiConfigParameters.java @@ -0,0 +1,57 @@ +package com.tungstun.gateway.swagger; + +import org.apache.commons.lang.WordUtils; +import org.springdoc.core.SwaggerUiConfigParameters; +import org.springdoc.core.SwaggerUiConfigProperties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +/** + * Class extending {@code SwaggerUiConfigParameters} to override the config parameters used by swagger UI. + */ +@Primary +@Configuration +public class BartapSwaggerUiConfigParameters extends SwaggerUiConfigParameters { + private static final String SERVICE_API_DOCS_URL_PREFIX = "/api/v3/api-docs/services/"; + + private final ServiceDefinitionsContext definitionContext; + private final String applicationName; + + public BartapSwaggerUiConfigParameters( + SwaggerUiConfigProperties swaggerUiConfig, + ServiceDefinitionsContext definitionContext, + @Value("${spring.application.name:Application}") String applicationName + ) { + super(swaggerUiConfig); + this.definitionContext = definitionContext; + this.applicationName = applicationName; + } + + /** + * Calls the original {@code getConfigParameters()} method to get the config parameters and overrides the {@code urls} value with a custom list of {@code SwaggerUrl}. + * The swagger urls contain the currently cached service names with a link that Swagger UI uses to retrieve their OpenApi Definition. + */ + @Override + public Map getConfigParameters() { + List swaggerUrls = definitionContext.getServiceIds() + .stream() + .map(this::toSwaggerUrl) + .collect(toList()); + + Map configParameters = super.getConfigParameters(); + swaggerUrls.add(new SwaggerUrl(applicationName, (String) configParameters.get(URL_PROPERTY), WordUtils.capitalize(applicationName))); + configParameters.put(URLS_PROPERTY, swaggerUrls); + return configParameters; + } + + private SwaggerUrl toSwaggerUrl(String serviceName) { + return new SwaggerUrl(serviceName, SERVICE_API_DOCS_URL_PREFIX + serviceName, WordUtils.capitalize(serviceName)); + } + +} diff --git a/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionResourceNotFoundException.java b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionResourceNotFoundException.java new file mode 100644 index 0000000..cf6a5da --- /dev/null +++ b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionResourceNotFoundException.java @@ -0,0 +1,11 @@ +package com.tungstun.gateway.swagger; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Exception to be thrown if a requested service OpenApi Definition does not exist or something went wrong during it's retrieval. + */ +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ServiceDefinitionResourceNotFoundException extends RuntimeException { +} diff --git a/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsContext.java b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsContext.java new file mode 100644 index 0000000..5d4120a --- /dev/null +++ b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsContext.java @@ -0,0 +1,40 @@ +package com.tungstun.gateway.swagger; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + *

Code adapted from https://github.com/GnanaJeyam/microservice-patterns

+ *

+ * Singleton class that stores an In-Memory map with service id's paired with their OpenApi Definition JSON String + */ +@Component +@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) +public class ServiceDefinitionsContext { + private ConcurrentMap serviceDefinitions; + + private ServiceDefinitionsContext() { + this.serviceDefinitions = new ConcurrentHashMap<>(); + } + + /** + * Changes the {@code serviceDefinitions} ConcurrentMap to the provided ConcurrentMap if it is not {@code null}. + */ + public void updateServiceDefinitions(ConcurrentMap map) { + if (map != null) serviceDefinitions = map; + } + + public String getSwaggerDefinition(String serviceId) { + return this.serviceDefinitions.get(serviceId); + } + + public List getServiceIds() { + return new ArrayList<>(serviceDefinitions.keySet()); + } +} diff --git a/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsUpdater.java b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsUpdater.java new file mode 100644 index 0000000..508e767 --- /dev/null +++ b/gateway/src/main/java/com/tungstun/gateway/swagger/ServiceDefinitionsUpdater.java @@ -0,0 +1,95 @@ +package com.tungstun.gateway.swagger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.graalvm.collections.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + *

Code adapted from https://github.com/GnanaJeyam/microservice-patterns

+ *

+ * Class that periodically updates the {@code ServiceDefinitionContext}. + */ +@Component +public class ServiceDefinitionsUpdater { + private static final Logger LOG = LoggerFactory.getLogger(ServiceDefinitionsUpdater.class); + private static final String DEFAULT_SWAGGER_URL = "/api/v3/api-docs"; + + private final ServiceDefinitionsContext definitionContext; + private final DiscoveryClient discoveryClient; + private final RestTemplate template; + + @Value("${spring.application.name}") + private String applicationName; + + public ServiceDefinitionsUpdater(ServiceDefinitionsContext definitionContext, DiscoveryClient discoveryClient) { + this.definitionContext = definitionContext; + this.discoveryClient = discoveryClient; + this.template = new RestTemplate(); + } + + /** + * Gets all running services from the service registry, polls their OpenApi Definition and updates the {@code ServiceDefinitionContext} with them. + */ + @Scheduled(fixedDelayString = "${swagger.config.refreshrate}") + public void refreshOpenApiDefinitions() { + LOG.info("Starting Service Definitions Context refresh"); + + ConcurrentMap serviceDocumentations = discoveryClient.getServices() + .parallelStream() + .filter(serviceId -> !serviceId.equals(applicationName)) + .map(serviceId -> Pair.create(serviceId, getServiceDefinition(serviceId))) + .filter(pair -> pair.getRight().isPresent()) + .collect(Collectors.toConcurrentMap(Pair::getLeft, pair -> pair.getRight().get())); + definitionContext.updateServiceDefinitions(serviceDocumentations); + + LOG.info("Service Definitions Context Refreshed for at : {}", LocalDateTime.now()); + } + + /** + * Gets a running service instance's URL and polls the OpenApi Definition from it. + */ + private Optional getServiceDefinition(String serviceId) { + List serviceInstances = discoveryClient.getInstances(serviceId); + if (serviceInstances == null || serviceInstances.isEmpty()) { + LOG.info("No instances available for service : {} ", serviceId); + return Optional.empty(); + } + + String swaggerURL = serviceInstances.get(0).getUri() + DEFAULT_SWAGGER_URL; + Optional jsonData = pollOpenApiDefinition(swaggerURL, serviceId); + if (jsonData.isEmpty()) { + LOG.error("Skipping Definition Refresh for service : {}", serviceId); + } + + return jsonData; + } + + /** + * Polls the OpenApi Definition JSON from the provided url. + */ + private Optional pollOpenApiDefinition(String url, String serviceName) { + try { + Object data = template.getForObject(url, Object.class); + String json = new ObjectMapper().writeValueAsString(data); + return Optional.of(json); + } catch (RestClientException | JsonProcessingException ex) { + LOG.error("Error while getting service definition for service : {}. Error : {} ", serviceName, ex.getMessage()); + return Optional.empty(); + } + } +} diff --git a/gateway/src/main/java/com/tungstun/gateway/swagger/SwaggerConfig.java b/gateway/src/main/java/com/tungstun/gateway/swagger/SwaggerConfig.java new file mode 100644 index 0000000..ce32eab --- /dev/null +++ b/gateway/src/main/java/com/tungstun/gateway/swagger/SwaggerConfig.java @@ -0,0 +1,77 @@ +package com.tungstun.gateway.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.apache.http.entity.ContentType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; + +import java.util.Optional; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; + +@Configuration +public class SwaggerConfig { + private final ServiceDefinitionsContext definitionContext; + + public SwaggerConfig(ServiceDefinitionsContext definitionContext) { + this.definitionContext = definitionContext; + } + + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Gateway") + .description("API Gateway of the bartap Backend API microservice cluster containing Centralized API Documentations.") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null && !gatewayUrl.isEmpty()) { + openApi.addServersItem(new Server() + .description("Security service") + .url(gatewayUrl) + ); + } + + return openApi; + } + + /** + * Router Function for Swagger UI to get the cached OpenApi Definition of running registered services + */ + @Bean + @Operation(hidden = true) + public RouterFunction serviceApiDocs() { + return RouterFunctions.route(GET("/v3/api-docs/services/{serviceName}"), (ServerRequest req) -> { + String service = req.pathVariable("serviceName"); + String swaggerDocs = Optional.ofNullable(definitionContext.getSwaggerDefinition(service)) + .orElseThrow(ServiceDefinitionResourceNotFoundException::new); + + return ServerResponse.ok() + .header("Content-Type", ContentType.APPLICATION_JSON.toString()) + .body(BodyInserters.fromValue(swaggerDocs)); + }); + } +} diff --git a/gateway/src/main/resources/application-ci.properties b/gateway/src/main/resources/application-ci.properties index 4900823..e69de29 100644 --- a/gateway/src/main/resources/application-ci.properties +++ b/gateway/src/main/resources/application-ci.properties @@ -1,6 +0,0 @@ -# Service Routes -BARTAP_SECURITY_URL=http://localhost:8081/api -BARTAP_CORE_URL=http://localhost:8082/api -BARTAP_PERSON_URL=http://localhost:8083/api -BARTAP_PRODUCT_URL=http://localhost:8084/api -BARTAP_ORDER_URL=http://localhost:8085/api \ No newline at end of file diff --git a/gateway/src/main/resources/application-dev.properties b/gateway/src/main/resources/application-dev.properties index 4900823..8b35d57 100644 --- a/gateway/src/main/resources/application-dev.properties +++ b/gateway/src/main/resources/application-dev.properties @@ -1,6 +1,2 @@ -# Service Routes -BARTAP_SECURITY_URL=http://localhost:8081/api -BARTAP_CORE_URL=http://localhost:8082/api -BARTAP_PERSON_URL=http://localhost:8083/api -BARTAP_PRODUCT_URL=http://localhost:8084/api -BARTAP_ORDER_URL=http://localhost:8085/api \ No newline at end of file +# Springdoc OpenAPI / Swagger +swagger.config.refreshrate=30000 diff --git a/gateway/src/main/resources/application-prod.properties b/gateway/src/main/resources/application-prod.properties index d939517..e288bf3 100644 --- a/gateway/src/main/resources/application-prod.properties +++ b/gateway/src/main/resources/application-prod.properties @@ -1,9 +1,2 @@ # Locale and timezones com.tungstun.bartap.locale.timezone=${TIMEZONE:Europe/Amsterdam} - -# Service Routes -BARTAP_SECURITY_URL=http://localhost:8081/api -BARTAP_CORE_URL=http://localhost:8082/api -BARTAP_PERSON_URL=http://localhost:8083/api -BARTAP_PRODUCT_URL=http://localhost:8084/api -BARTAP_ORDER_URL=http://localhost:8085/api \ No newline at end of file diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index f5aa00c..1d0e531 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -1,18 +1,23 @@ #Standard configurations -spring.application.name=bartap-gateway +spring.application.name=gateway spring.profiles.active=dev server.port=8081 -server.servlet.context-path=/api +#server.servlet.context-path=/api +spring.webflux.base-path=/api spring.main.web-application-type=reactive spring.cloud.gateway.enabled=true - # Service Routes -com.tungstun.bartap.routes.security=${BARTAP_SECURITY_URL:http://localhost:8084/api} -com.tungstun.bartap.routes.core=${BARTAP_CORE_URL:http://localhost:8082/api} -com.tungstun.bartap.routes.bill=${BARTAP_BILL_URL:http://localhost:8083/api} -com.tungstun.bartap.routes.person=${BARTAP_PERSON_URL:http://localhost:8084/api} -com.tungstun.bartap.routes.product=${BARTAP_PRODUCT_URL:http://localhost:8085/api} -com.tungstun.bartap.routes.order=${BARTAP_ORDER_URL:http://localhost:8086/api} - - - +com.tungstun.services.security=lb://BARTAP-SECURITY/api +com.tungstun.services.core=lb://BARTAP-CORE/api +com.tungstun.services.bill=lb://BARTAP-BILL/api +com.tungstun.services.person=lb://BARTAP-PERSON/api +com.tungstun.services.product=lb://BARTAP-PRODUCT/api +com.tungstun.services.order=lb://BARTAP-ORDER/api +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true +# Springdoc OpenAPI / Swagger +springdoc.swagger-ui.path=/swagger +# 5 minutes refresh delay +swagger.config.refreshrate=300000 diff --git a/person/build.gradle b/person/build.gradle index f22f283..62c3517 100644 --- a/person/build.gradle +++ b/person/build.gradle @@ -12,4 +12,14 @@ repositories { dependencies { implementation project(':common') annotationProcessor project(':common') + +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Open Api UI dependency + implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' } diff --git a/person/src/main/java/com/tungstun/person/PersonApplication.java b/person/src/main/java/com/tungstun/person/PersonApplication.java index 7dba5cb..ad076fb 100644 --- a/person/src/main/java/com/tungstun/person/PersonApplication.java +++ b/person/src/main/java/com/tungstun/person/PersonApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.kafka.annotation.EnableKafka; @SpringBootApplication @ComponentScan("com.tungstun") @EnableKafka +@EnableEurekaClient public class PersonApplication { public static void main(String[] args) { SpringApplication.run(PersonApplication.class, args); diff --git a/person/src/main/java/com/tungstun/person/config/SwaggerConfig.java b/person/src/main/java/com/tungstun/person/config/SwaggerConfig.java index 37a9d33..28bf892 100644 --- a/person/src/main/java/com/tungstun/person/config/SwaggerConfig.java +++ b/person/src/main/java/com/tungstun/person/config/SwaggerConfig.java @@ -1,26 +1,45 @@ package com.tungstun.person.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.models.annotations.OpenAPI31; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@OpenAPI31 -@OpenAPIDefinition( - info = @Info( - title = "bartap Backend Api - Person", - description = "Person API of the bartap Backend API microservice cluster containing person functionality", - version = "1.0", - contact = @Contact( - name = "Tungstun", - url = "https://github.com/tungstun-ict", - email = "jort@tungstun.nl" - ) - ), - tags = { - @Tag(name = "Person", description = "Functionality based around the Person") - } -) +@Configuration public class SwaggerConfig { + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Person") + .description("Person API of the bartap Backend API microservice cluster containing person functionality") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .addTagsItem(new Tag().name("Person").description("Functionality based around the Person")) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null) { + openApi.addServersItem(new Server() + .description("Gateway Url") + .url(gatewayUrl) + ); + } + + return openApi; + } } diff --git a/person/src/main/resources/application-dev.properties b/person/src/main/resources/application-dev.properties index 965a2e6..88f3129 100644 --- a/person/src/main/resources/application-dev.properties +++ b/person/src/main/resources/application-dev.properties @@ -4,3 +4,5 @@ JWT_ISSUER=bartap-security-service #Database properties and variables JDBC_DATABASE_URL=jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 DRIVER_CLASS_NAME=org.h2.Driver +#Gateway url +GATEWAY_URL=http://localhost:8081${server.servlet.context-path} diff --git a/person/src/main/resources/application.properties b/person/src/main/resources/application.properties index 74ef4b3..ffd6554 100644 --- a/person/src/main/resources/application.properties +++ b/person/src/main/resources/application.properties @@ -1,7 +1,8 @@ -spring.application.name=bartap-product +spring.application.name=person spring.profiles.active=dev -server.port=8085 +server.port=8086 server.servlet.context-path=/api + #JWT variables com.tungstun.bartap.security.jwt.jwtSecret=${JWT_SECRET} com.tungstun.bartap.security.jwt.jwtIssuer=${JWT_ISSUER} @@ -15,3 +16,7 @@ spring.datasource.password=${JDBC_DATABASE_PASSWORD} # Kafka spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVERS:localhost:9092} spring.kafka.consumer.auto-offset-reset=earliest +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true diff --git a/product/build.gradle b/product/build.gradle index aa86021..ea9a303 100644 --- a/product/build.gradle +++ b/product/build.gradle @@ -13,6 +13,16 @@ dependencies { implementation project(':common') annotationProcessor project(':common') - implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Apache Commons Text implementation 'org.apache.commons:commons-text:1.9' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Open Api UI dependency + implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' } diff --git a/product/src/main/java/com/tungstun/product/ProductApplication.java b/product/src/main/java/com/tungstun/product/ProductApplication.java index 1e15569..0fe5d51 100644 --- a/product/src/main/java/com/tungstun/product/ProductApplication.java +++ b/product/src/main/java/com/tungstun/product/ProductApplication.java @@ -2,10 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.ComponentScan; +import org.springframework.kafka.annotation.EnableKafka; @SpringBootApplication @ComponentScan("com.tungstun") +@EnableKafka +@EnableEurekaClient public class ProductApplication { public static void main(String[] args) { diff --git a/product/src/main/java/com/tungstun/product/config/SwaggerConfig.java b/product/src/main/java/com/tungstun/product/config/SwaggerConfig.java index 2f84971..7b0e1d2 100644 --- a/product/src/main/java/com/tungstun/product/config/SwaggerConfig.java +++ b/product/src/main/java/com/tungstun/product/config/SwaggerConfig.java @@ -1,27 +1,46 @@ package com.tungstun.product.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.models.annotations.OpenAPI31; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@OpenAPI31 -@OpenAPIDefinition( - info = @Info( - title = "bartap Backend Api - Product", - description = "Product API of the bartap Backend API microservice cluster containing product and category functionality", - version = "1.0", - contact = @Contact( - name = "Tungstun", - url = "https://github.com/tungstun-ict", - email = "jort@tungstun.nl" - ) - ), - tags = { - @Tag(name = "Category", description = "Functionality based around the Category"), - @Tag(name = "Product", description = "Functionality based around the Product") - } -) +@Configuration public class SwaggerConfig { + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Product") + .description("Product API of the bartap Backend API microservice cluster containing product and category functionality") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .addTagsItem(new Tag().name("Category").description("Functionality based around the Category")) + .addTagsItem(new Tag().name("Product").description("Functionality based around the Product")) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null) { + openApi.addServersItem(new Server() + .description("Gateway Url") + .url(gatewayUrl) + ); + } + + return openApi; + } } diff --git a/product/src/main/resources/application-dev.properties b/product/src/main/resources/application-dev.properties index de1262c..88f3129 100644 --- a/product/src/main/resources/application-dev.properties +++ b/product/src/main/resources/application-dev.properties @@ -1,7 +1,8 @@ #JWT variables JWT_SECRET=lets-make-a-bar-application-and-use-this-as-a-session-token-secret JWT_ISSUER=bartap-security-service - #Database properties and variables JDBC_DATABASE_URL=jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 DRIVER_CLASS_NAME=org.h2.Driver +#Gateway url +GATEWAY_URL=http://localhost:8081${server.servlet.context-path} diff --git a/product/src/main/resources/application.properties b/product/src/main/resources/application.properties index 378f9fa..56cfbbf 100644 --- a/product/src/main/resources/application.properties +++ b/product/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.application.name=bartap-product +spring.application.name=product spring.profiles.active=dev server.port=8085 server.servlet.context-path=/api @@ -14,7 +14,10 @@ spring.datasource.driverClassName=${DRIVER_CLASS_NAME} spring.datasource.url=${JDBC_DATABASE_URL} spring.datasource.username=${JDBC_DATABASE_USERNAME} spring.datasource.password=${JDBC_DATABASE_PASSWORD} - # Kafka spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVERS:localhost:9092} spring.kafka.consumer.auto-offset-reset=earliest +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true diff --git a/security/build.gradle b/security/build.gradle index fb564b8..974f21f 100644 --- a/security/build.gradle +++ b/security/build.gradle @@ -14,8 +14,18 @@ dependencies { implementation project(':common') annotationProcessor project(':common') - implementation 'com.auth0:java-jwt:3.19.2' +// Spring dependencies + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + +// Eureka Service Registry + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.3' + +// Open Api UI dependency implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' + +// Auth0 JWT + implementation 'com.auth0:java-jwt:3.19.2' } diff --git a/security/src/main/java/com/tungstun/security/SecurityApplication.java b/security/src/main/java/com/tungstun/security/SecurityApplication.java index 24209b6..975b406 100644 --- a/security/src/main/java/com/tungstun/security/SecurityApplication.java +++ b/security/src/main/java/com/tungstun/security/SecurityApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.kafka.annotation.EnableKafka; @SpringBootApplication @ComponentScan("com.tungstun") @EnableKafka +@EnableEurekaClient public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class, args); diff --git a/security/src/main/java/com/tungstun/security/config/SwaggerConfig.java b/security/src/main/java/com/tungstun/security/config/SwaggerConfig.java index 50bd90e..65208ed 100644 --- a/security/src/main/java/com/tungstun/security/config/SwaggerConfig.java +++ b/security/src/main/java/com/tungstun/security/config/SwaggerConfig.java @@ -1,28 +1,47 @@ package com.tungstun.security.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.models.annotations.OpenAPI31; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@OpenAPI31 -@OpenAPIDefinition( - info = @Info( - title = "bartap Backend Api - Security", - description = "Security API of the bartap Backend API microservice cluster containing user and authorization functionality", - version = "1.0", - contact = @Contact( - name = "Tungstun", - url = "https://github.com/tungstun-ict", - email = "jort@tungstun.nl" - ) - ), - tags = { - @Tag(name = "User", description = "Functionality based around the user"), - @Tag(name = "Authentication", description = "Functionality based around the authentication"), - @Tag(name = "Authorization", description = "Functionality based around the authorization"), - } -) +@Configuration public class SwaggerConfig { + @Bean + public OpenAPI gatewayOpenApi(@Value("${GATEWAY_URL:}") String gatewayUrl) { + OpenAPI openApi = new OpenAPI(SpecVersion.V31) + .info(new Info() + .title("bartap Backend Api - Security") + .description("Security API of the bartap Backend API microservice cluster containing user and authorization functionality") + .version("1.0") + .contact(new Contact() + .name("Tungstun") + .url("https://github.com/tungstun-ict") + .email("jort@tungstun.nl"))) + .addTagsItem(new Tag().name("User").description("Functionality based around the user")) + .addTagsItem(new Tag().name("Authentication").description("Functionality based around the authentication")) + .addTagsItem(new Tag().name("Authorization").description("Functionality based around the authorization")) + .schemaRequirement("Bearer", new SecurityScheme() + .name("Bearer") + .description("Authorization using Bearer JWT") + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT")); + + if (gatewayUrl != null) { + openApi.addServersItem(new Server() + .description("Gateway Url") + .url(gatewayUrl) + ); + } + + return openApi; + } } diff --git a/security/src/main/java/com/tungstun/security/domain/user/User.java b/security/src/main/java/com/tungstun/security/domain/user/User.java index 49018d0..3db599c 100644 --- a/security/src/main/java/com/tungstun/security/domain/user/User.java +++ b/security/src/main/java/com/tungstun/security/domain/user/User.java @@ -54,13 +54,13 @@ public User(String username, String password, String mail, String firstName, Str } public void canAuthenticate() { - if (isAccountNonExpired()) + if (!isAccountNonExpired()) throw new CannotAuthenticateException("Account expired. An expired account cannot be authenticated."); - if (isAccountNonLocked()) + if (!isAccountNonLocked()) throw new CannotAuthenticateException("Account locked. A locked account cannot be authenticated."); - if (isCredentialsNonExpired()) + if (!isCredentialsNonExpired()) throw new CannotAuthenticateException("Account credentials expired. Expired credentials prevent authentication."); - if (isEnabled()) + if (!isEnabled()) throw new CannotAuthenticateException("Account disabled. A disabled account cannot be authenticated."); } diff --git a/security/src/main/resources/application-dev.properties b/security/src/main/resources/application-dev.properties index 7c0c613..12eaa17 100644 --- a/security/src/main/resources/application-dev.properties +++ b/security/src/main/resources/application-dev.properties @@ -4,7 +4,8 @@ JWT_EXPIRATION_DATE_IN_MS=300000 JWT_REFRESH_EXPIRATION_DATE_IN_MS=3000000 JWT_ISSUER=bartap-security-service JWT_AUDIENCE=bartap-security-service - #Database properties and variables JDBC_DATABASE_URL=jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 DRIVER_CLASS_NAME=org.h2.Driver +#Gateway url +GATEWAY_URL=http://localhost:8081${server.servlet.context-path} diff --git a/security/src/main/resources/application.properties b/security/src/main/resources/application.properties index 8d47762..fbe9215 100644 --- a/security/src/main/resources/application.properties +++ b/security/src/main/resources/application.properties @@ -1,5 +1,5 @@ #Standard configurations -spring.application.name=bartap-security +spring.application.name=security spring.profiles.active=dev server.port=8084 server.servlet.context-path=/api @@ -17,7 +17,10 @@ spring.datasource.driverClassName=${DRIVER_CLASS_NAME} spring.datasource.url=${JDBC_DATABASE_URL} spring.datasource.username=${JDBC_DATABASE_USERNAME} spring.datasource.password=${JDBC_DATABASE_PASSWORD} - # Kafka spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVERS:localhost:9092} -spring.kafka.consumer.auto-offset-reset=earliest \ No newline at end of file +spring.kafka.consumer.auto-offset-reset=earliest +# Eureka Registry +eureka.client.service-url.defaultZone=http://localhost:8761/eureka +eureka.client.fetch-registry=true +eureka.client.register-with-eureka=true diff --git a/settings.gradle b/settings.gradle index 0484f89..8db7ac6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'bartap' -include('bill', 'common', 'core', 'gateway', 'person', 'product', 'security') +include('bill', 'common', 'core', 'eureka-server', 'gateway', 'person', 'product', 'security')