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