diff --git a/library-api-books-service/src/main/java/dev/earlspilner/books/entity/Book.java b/library-api-books-service/src/main/java/dev/earlspilner/books/entity/Book.java
index af12d34..957bbfb 100644
--- a/library-api-books-service/src/main/java/dev/earlspilner/books/entity/Book.java
+++ b/library-api-books-service/src/main/java/dev/earlspilner/books/entity/Book.java
@@ -38,10 +38,6 @@ public class Book {
@Column(name = "author", nullable = false)
private String author;
- @Setter
- @Column(name = "available", nullable = false)
- private Boolean available = true;
-
@Setter
@Column(name = "appearedUtc", nullable = false, updatable = false)
private Instant appearedUtc;
@@ -54,14 +50,13 @@ protected void onCreate() {
public Book() {
}
- public Book(Integer id, String isbn, String title, String genre, String description, String author, Boolean available, Instant appearedUtc) {
+ public Book(Integer id, String isbn, String title, String genre, String description, String author, Instant appearedUtc) {
this.id = id;
this.isbn = isbn;
this.title = title;
this.genre = genre;
this.description = description;
this.author = author;
- this.available = available;
this.appearedUtc = appearedUtc;
}
@@ -74,7 +69,6 @@ public String toString() {
.append("genre", genre)
.append("description", description)
.append("author", author)
- .append("available", available)
.append("appearedUtc", appearedUtc)
.toString();
}
diff --git a/library-api-gateway/pom.xml b/library-api-gateway/pom.xml
new file mode 100644
index 0000000..e172606
--- /dev/null
+++ b/library-api-gateway/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ library-api-gateway
+ jar
+ Library API Gateway
+
+
+ dev.earlspilner
+ library-api
+ 1.1.0
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-hystrix
+ 2.2.10.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+
+
+
+
\ No newline at end of file
diff --git a/library-api-gateway/src/main/java/dev/earlspilner/gateway/ApiGatewayApplication.java b/library-api-gateway/src/main/java/dev/earlspilner/gateway/ApiGatewayApplication.java
new file mode 100644
index 0000000..073ba43
--- /dev/null
+++ b/library-api-gateway/src/main/java/dev/earlspilner/gateway/ApiGatewayApplication.java
@@ -0,0 +1,18 @@
+package dev.earlspilner.gateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * @author Alexander Dudkin
+ */
+@EnableDiscoveryClient
+@SpringBootApplication
+public class ApiGatewayApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ApiGatewayApplication.class, args);
+ }
+
+}
diff --git a/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/AuthenticationFilter.java b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/AuthenticationFilter.java
new file mode 100644
index 0000000..5e45d50
--- /dev/null
+++ b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/AuthenticationFilter.java
@@ -0,0 +1,74 @@
+package dev.earlspilner.gateway.config;
+
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Alexander Dudkin
+ */
+@Component
+@RefreshScope
+public class AuthenticationFilter implements GatewayFilter {
+
+ private final RouterValidator routerValidator;
+ private final JwtUtil jwtUtil;
+
+ @Autowired
+ public AuthenticationFilter(RouterValidator routerValidator, JwtUtil jwtUtil) {
+ this.routerValidator = routerValidator;
+ this.jwtUtil = jwtUtil;
+ }
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ ServerHttpRequest request = exchange.getRequest();
+
+ if (routerValidator.isSecured.test(request)) {
+ if (this.isAuthMissing(request)) {
+ return this.onError(exchange, HttpStatus.UNAUTHORIZED);
+ }
+
+ final String token = this.getAuthHeader(request);
+
+ if (jwtUtil.isInvalid(token)) {
+ return this.onError(exchange, HttpStatus.FORBIDDEN);
+ }
+
+ this.updateRequest(exchange, token);
+ }
+
+ return chain.filter(exchange);
+ }
+
+ private Mono onError(ServerWebExchange exchange, HttpStatus httpStatus) {
+ ServerHttpResponse response = exchange.getResponse();
+ response.setStatusCode(httpStatus);
+ return response.setComplete();
+ }
+
+ private String getAuthHeader(ServerHttpRequest request) {
+ String header = request.getHeaders().getOrEmpty("Authorization").get(0);
+ return header.startsWith("Bearer ") ? header.substring(7) : header;
+ }
+
+ private boolean isAuthMissing(ServerHttpRequest request) {
+ return !request.getHeaders().containsKey("Authorization");
+ }
+
+ private void updateRequest(ServerWebExchange exchange, String token) {
+ Claims claims = jwtUtil.getAllClaimsFromToken(token);
+ exchange.getRequest().mutate()
+ .header("username", String.valueOf(claims.get("username")))
+ .build();
+ }
+
+}
diff --git a/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/GatewayConfig.java b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/GatewayConfig.java
new file mode 100644
index 0000000..17834c5
--- /dev/null
+++ b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/GatewayConfig.java
@@ -0,0 +1,51 @@
+package dev.earlspilner.gateway.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.cloud.netflix.hystrix.EnableHystrix;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Alexander Dudkin
+ */
+@Configuration
+@EnableHystrix
+public class GatewayConfig {
+
+ static final String USERS_SERVICE = "http://localhost:9091";
+ static final String AUTH_SERVER = "http://localhost:6969";
+ static final String BOOKS_SERVICE = "http://localhost:9092";
+ static final String LOAN_SERVICE = "http://localhost:9093";
+ static final String LIBRARY_SERVICE = "http://localhost:9094";
+
+ private final AuthenticationFilter filter;
+
+ @Autowired
+ public GatewayConfig(AuthenticationFilter filter) {
+ this.filter = filter;
+ }
+
+ @Bean
+ public RouteLocator routes(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("users-service", r -> r.path("/api/users/**")
+ .filters(f -> f.filter(filter))
+ .uri(USERS_SERVICE))
+ .route("auth-server", r -> r.path("/api/auth/**")
+ .filters(f -> f.filter(filter))
+ .uri(AUTH_SERVER))
+ .route("books-service", r -> r.path("/api/books/**")
+ .filters(f -> f.filter(filter))
+ .uri(BOOKS_SERVICE))
+ .route("loan-service", r -> r.path("/api/loans/**")
+ .filters(f -> f.filter(filter))
+ .uri(LOAN_SERVICE))
+ .route("library-service", r -> r.path("/api/library/**")
+ .filters(f -> f.filter(filter))
+ .uri(LIBRARY_SERVICE))
+ .build();
+ }
+
+}
diff --git a/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/JwtUtil.java b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/JwtUtil.java
new file mode 100644
index 0000000..9aa39cd
--- /dev/null
+++ b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/JwtUtil.java
@@ -0,0 +1,43 @@
+package dev.earlspilner.gateway.config;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.security.Key;
+import java.util.Base64;
+import java.util.Date;
+
+/**
+ * @author Alexander Dudkin
+ */
+@Component
+public class JwtUtil {
+
+ @Value("${jwt.secret.key}")
+ private String jwtSecret;
+
+ private Key key;
+
+ @PostConstruct
+ protected void init() {
+ byte[] keyBytes = Base64.getDecoder().decode(jwtSecret.getBytes());
+ this.key = Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ public Claims getAllClaimsFromToken(String token) {
+ return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
+ }
+
+ private boolean isTokenExpired(String token) {
+ return this.getAllClaimsFromToken(token).getExpiration().before(new Date());
+ }
+
+ public boolean isInvalid(String token) {
+ return this.isTokenExpired(token);
+ }
+
+}
\ No newline at end of file
diff --git a/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/RouterValidator.java b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/RouterValidator.java
new file mode 100644
index 0000000..1bbf60a
--- /dev/null
+++ b/library-api-gateway/src/main/java/dev/earlspilner/gateway/config/RouterValidator.java
@@ -0,0 +1,24 @@
+package dev.earlspilner.gateway.config;
+
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * @author Alexander Dudkin
+ */
+@Component
+public class RouterValidator {
+
+ public static final List openApiEndpoints = List.of(
+ "/api/auth/login",
+ "/api/auth/refresh",
+ "/api/users"
+ );
+
+ public Predicate isSecured = request -> openApiEndpoints.stream()
+ .noneMatch(uri -> request.getURI().getPath().contains(uri));
+
+}
diff --git a/library-api-gateway/src/main/resources/application.yml b/library-api-gateway/src/main/resources/application.yml
new file mode 100644
index 0000000..3e72aac
--- /dev/null
+++ b/library-api-gateway/src/main/resources/application.yml
@@ -0,0 +1,6 @@
+spring:
+ application:
+ name: api-gateway
+
+server:
+ port: 8080
\ No newline at end of file
diff --git a/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryApi.java b/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryApi.java
index 561e642..9be35ed 100644
--- a/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryApi.java
+++ b/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryApi.java
@@ -10,4 +10,5 @@ public interface LibraryApi {
ResponseEntity addBootRecord(BookRecordDto dto);
ResponseEntity getBookRecord(Integer bookId);
ResponseEntity updateBookRecord(Integer bookId, BookRecordDto dto);
+ ResponseEntity deleteBookRecord(Integer bookId);
}
diff --git a/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryRestController.java b/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryRestController.java
index 620c9d4..def2a2b 100644
--- a/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryRestController.java
+++ b/library-api-library-service/src/main/java/dev/earlspilner/library/rest/controller/LibraryRestController.java
@@ -40,4 +40,11 @@ public ResponseEntity updateBookRecord(@PathVariable Integer book
return new ResponseEntity<>(libraryService.updateBookRecord(bookId, dto), HttpStatus.OK);
}
+ @Override
+ @DeleteMapping("/{bookId}")
+ public ResponseEntity deleteBookRecord(@PathVariable Integer bookId) {
+ libraryService.deleteBookRecord(bookId);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+
}
diff --git a/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryService.java b/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryService.java
index 2e6710b..49b66ab 100644
--- a/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryService.java
+++ b/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryService.java
@@ -9,4 +9,5 @@ public interface LibraryService {
BookRecordDto addBookRecord(BookRecordDto dto);
BookRecordDto getBookRecord(Integer id);
BookRecordDto updateBookRecord(Integer bookId, BookRecordDto dto);
+ void deleteBookRecord(Integer bookId);
}
diff --git a/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryServiceImpl.java b/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryServiceImpl.java
index 454909b..f8576b0 100644
--- a/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryServiceImpl.java
+++ b/library-api-library-service/src/main/java/dev/earlspilner/library/service/LibraryServiceImpl.java
@@ -49,4 +49,9 @@ public BookRecordDto updateBookRecord(Integer bookId, BookRecordDto dto) {
return bookRecordMapper.toDto(bookRecordRepository.save(bookRecord));
}
+ @Override
+ public void deleteBookRecord(Integer bookId) {
+ bookRecordRepository.deleteById(bookId);
+ }
+
}
diff --git a/library-api-loan-service/src/main/java/dev/earlspilner/loans/repository/LoanRepository.java b/library-api-loan-service/src/main/java/dev/earlspilner/loans/repository/LoanRepository.java
index 83c16ca..2569909 100644
--- a/library-api-loan-service/src/main/java/dev/earlspilner/loans/repository/LoanRepository.java
+++ b/library-api-loan-service/src/main/java/dev/earlspilner/loans/repository/LoanRepository.java
@@ -9,5 +9,5 @@
* @author Alexander Dudkin
*/
public interface LoanRepository extends JpaRepository {
- Optional findByUserIdAndBookIdAndReturnedAtIsNull(Integer bookId, Integer userId);
+ Optional findByBookIdAndUserIdAndReturnedAtIsNull(Integer bookId, Integer userId);
}
diff --git a/library-api-loan-service/src/main/java/dev/earlspilner/loans/service/LoanServiceImpl.java b/library-api-loan-service/src/main/java/dev/earlspilner/loans/service/LoanServiceImpl.java
index 85e6f77..3769d60 100644
--- a/library-api-loan-service/src/main/java/dev/earlspilner/loans/service/LoanServiceImpl.java
+++ b/library-api-loan-service/src/main/java/dev/earlspilner/loans/service/LoanServiceImpl.java
@@ -70,7 +70,7 @@ public LoanDto returnBook(Integer bookId, HttpServletRequest request) {
}
UserDto userDto = userClient.getUser(jwtCore.getUsernameFromToken(jwtCore.getTokenFromRequest(request)));
- Loan loan = loanRepository.findByUserIdAndBookIdAndReturnedAtIsNull(bookId, userDto.id())
+ Loan loan = loanRepository.findByBookIdAndUserIdAndReturnedAtIsNull(bookId, userDto.id())
.orElseThrow(() -> new LoanNotFoundException("Loan not found with bookId '" + bookId + "' and userId '" + userDto.id() + "'"));
loan.setReturnedAt(Instant.now());
libraryClient.setBookStatus(bookId, new BookRecordDto(null, IN_LIBRARY));
diff --git a/pom.xml b/pom.xml
index c279abe..a8babca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,7 @@
library-api-library-service
library-api-discovery-server
library-api-loan-service
+ library-api-gateway
dev.earlspilner