diff --git a/.gitignore b/.gitignore index d41fd5e..46db3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ DataAccess.iml Presentation.iml Application.iml target +DataAccessApiGateway.iml +ApiGateway.iml +ApplicationApiGateway.iml ### VS Code ### .vscode/ diff --git a/ApiGateway/ApplicationApiGateway/pom.xml b/ApiGateway/ApplicationApiGateway/pom.xml new file mode 100644 index 0000000..cc671ed --- /dev/null +++ b/ApiGateway/ApplicationApiGateway/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.example + ApiGateway + 1.0-SNAPSHOT + + + ApplicationApiGateway + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/ApiGateway/ApplicationApiGateway/src/main/java/Models/Entities/User.java b/ApiGateway/ApplicationApiGateway/src/main/java/Models/Entities/User.java new file mode 100644 index 0000000..8eca4f7 --- /dev/null +++ b/ApiGateway/ApplicationApiGateway/src/main/java/Models/Entities/User.java @@ -0,0 +1,36 @@ +package Models.Entities; + +import Models.Enums.Role; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "users") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Getter + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Getter + private Role role; + + public User(String username, String pw, Role role) { + this.username = username; + this.password = pw; + this.role = role; + } +} diff --git a/ApiGateway/ApplicationApiGateway/src/main/java/Models/Enums/Role.java b/ApiGateway/ApplicationApiGateway/src/main/java/Models/Enums/Role.java new file mode 100644 index 0000000..e1f4b5a --- /dev/null +++ b/ApiGateway/ApplicationApiGateway/src/main/java/Models/Enums/Role.java @@ -0,0 +1,6 @@ +package Models.Enums; + +public enum Role { + ADMIN, + CLIENT +} diff --git a/ApiGateway/DataAccessApiGateway/pom.xml b/ApiGateway/DataAccessApiGateway/pom.xml new file mode 100644 index 0000000..593fa2b --- /dev/null +++ b/ApiGateway/DataAccessApiGateway/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.example + ApiGateway + 1.0-SNAPSHOT + + + DataAccessApiGateway + + + 21 + 21 + UTF-8 + + + + + com.example + ApplicationApiGateway + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/ApiGateway/DataAccessApiGateway/src/main/java/Repositories/UserRepository.java b/ApiGateway/DataAccessApiGateway/src/main/java/Repositories/UserRepository.java new file mode 100644 index 0000000..a00bcc1 --- /dev/null +++ b/ApiGateway/DataAccessApiGateway/src/main/java/Repositories/UserRepository.java @@ -0,0 +1,12 @@ +package Repositories; + +import Models.Entities.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/ApiGateway/DataAccessApiGateway/src/main/java/Services/CustomUserDetailService.java b/ApiGateway/DataAccessApiGateway/src/main/java/Services/CustomUserDetailService.java new file mode 100644 index 0000000..71574be --- /dev/null +++ b/ApiGateway/DataAccessApiGateway/src/main/java/Services/CustomUserDetailService.java @@ -0,0 +1,34 @@ +package Services; + +import Repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailService implements UserDetailsService { + private final UserRepository userRepository; + + @Autowired + public CustomUserDetailService(UserRepository repository) { + this.userRepository = repository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + var user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User is not found.")); + + String role = "ROLE_" + user.getRole().toString(); + + return org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + .authorities(new SimpleGrantedAuthority(role)) + .build(); + } + +} diff --git a/ApiGateway/DataAccessApiGateway/src/main/java/Services/UserService.java b/ApiGateway/DataAccessApiGateway/src/main/java/Services/UserService.java new file mode 100644 index 0000000..f4a9955 --- /dev/null +++ b/ApiGateway/DataAccessApiGateway/src/main/java/Services/UserService.java @@ -0,0 +1,31 @@ +package Services; + +import Models.Entities.User; +import Models.Enums.Role; +import Repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + private final UserRepository userRepository; + private final PasswordEncoder encoder; + + @Autowired + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { + this.userRepository = repository; + this.encoder = passwordEncoder; + } + + public User CreateUser(String username, String password, Role role) { + System.out.println("Creating user: " + username + " with role: " + role); + String pwHash = encoder.encode(password); + User user = new User(username, pwHash, role); + return userRepository.save(user); + } + + public User FindUserByUsername(String username) { + return userRepository.findByUsername(username).orElse(null); + } +} diff --git a/ApiGateway/PresentationApiGateway/pom.xml b/ApiGateway/PresentationApiGateway/pom.xml new file mode 100644 index 0000000..0276eb5 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.example + ApiGateway + 1.0-SNAPSHOT + + + PresentationApiGateway + + + 21 + 21 + UTF-8 + + + + + + + com.example + DataAccessApiGateway + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + Console.ApiGatewayApp + + + + + + \ No newline at end of file diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Configs/RestTemplateConfig.java b/ApiGateway/PresentationApiGateway/src/main/java/Configs/RestTemplateConfig.java new file mode 100644 index 0000000..802a70f --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Configs/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package Configs; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Configs/SecurityConfig.java b/ApiGateway/PresentationApiGateway/src/main/java/Configs/SecurityConfig.java new file mode 100644 index 0000000..77aafab --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Configs/SecurityConfig.java @@ -0,0 +1,66 @@ +package Configs; + +import JWT.JwtAuthFilter; +import Services.CustomUserDetailService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.List; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + private final CustomUserDetailService userDetailService; + + public SecurityConfig(JwtAuthFilter jwtAuthFilter, CustomUserDetailService userDetailService) { + this.jwtAuthFilter = jwtAuthFilter; + this.userDetailService = userDetailService; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/login").permitAll() + .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") + .requestMatchers("/client/**").hasAuthority("ROLE_CLIENT") + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Добавляем фильтр JWT + + return http.build(); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(DaoAuthenticationProvider daoAuthProvider) { + return new ProviderManager(List.of(daoAuthenticationProvider())); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Console/ApiGatewayApp.java b/ApiGateway/PresentationApiGateway/src/main/java/Console/ApiGatewayApp.java new file mode 100644 index 0000000..05d88fb --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Console/ApiGatewayApp.java @@ -0,0 +1,23 @@ +package Console; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication(scanBasePackages = { + "Controllers", + "Configs", + "JWT", + "Services", + "Repositories", + "Models.Entities", + "Models.Enums" +}) +@EnableJpaRepositories(basePackages = "Repositories") +@EntityScan(basePackages = "Models.Entities") +public class ApiGatewayApp { + public static void main(String[] args) { + SpringApplication.run(ApiGatewayApp.class, args); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Console/PasswordEncoderTest.java b/ApiGateway/PresentationApiGateway/src/main/java/Console/PasswordEncoderTest.java new file mode 100644 index 0000000..cf26fa1 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Console/PasswordEncoderTest.java @@ -0,0 +1,12 @@ +package Console; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class PasswordEncoderTest { + public static void main(String[] args) { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String password = "admin123"; + String encodedPassword = encoder.encode(password); + System.out.println(encodedPassword); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AdminController.java b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AdminController.java new file mode 100644 index 0000000..ab1931f --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AdminController.java @@ -0,0 +1,89 @@ +package Controllers; + +import DTO.BankAccountDTO; +import DTO.OperationDTO; +import DTO.UserDTO; +import Requests.UserFilterRequest; +import Requests.UserRequest; +import Services.AdminService; +import Services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin") +public class AdminController { + private final UserService userService; + private final AdminService adminService; + + @Autowired + public AdminController(UserService userService, AdminService adminService) { + this.userService = userService; + this.adminService = adminService; + } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping("/users/create") + public ResponseEntity CreateUser(@RequestBody UserRequest userRequest) { + userService.CreateUser(userRequest.getUsername(), userRequest.getPassword(), userRequest.getRole()); + return ResponseEntity.ok("User created successfully."); + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/users") + public ResponseEntity> getAllUsersFiltered( + @RequestParam(required = false) String sex, + @RequestParam(required = false) String hairColor + ) { + UserFilterRequest request = new UserFilterRequest(); + request.setSex(sex); + request.setHairColor(hairColor); + return ResponseEntity.ok(adminService.getFilteredUsers(request)); + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/users/{id}") + public ResponseEntity getUserById(@PathVariable int id) { + return ResponseEntity.ok(adminService.getUserById(id)); + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/bankaccounts") + public ResponseEntity> getAllBankAccounts() { + return ResponseEntity.ok(adminService.getAllBankAccounts()); + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/bankaccounts/user/{userId}") + public ResponseEntity> getAccountsByUserId(@PathVariable int userId) { + return ResponseEntity.ok(adminService.getAccountsByUserId(userId)); + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/bankaccounts/{id}/operations") + public ResponseEntity> getOperationsByAccountId( + @PathVariable int id, + @RequestParam(required = false) String type + ) { + return ResponseEntity.ok(adminService.getOperationsByAccountId(id, type)); + } + + + @PreAuthorize("isAuthenticated()") + @PostMapping("/logout") + public ResponseEntity Logout() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return ResponseEntity.ok("Admin " + authentication.getName() + " logged out."); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(500).body("Error during logout: " + e.getMessage()); + } + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AuthController.java b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AuthController.java new file mode 100644 index 0000000..1da9b2c --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/AuthController.java @@ -0,0 +1,58 @@ +package Controllers; + +import JWT.JwtUtil; +import Requests.LoginRequest; +import Responses.JwtResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.bind.annotation.*; + +@RestController +public class AuthController { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Autowired + public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, UserDetailsService userDetailsService) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + @GetMapping("/login") + public ResponseEntity getLoginInfo() { + return ResponseEntity.status(HttpStatus.OK).body("Please send a POST request to /login with your credentials."); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + try { + if (loginRequest.getUsername() == null || loginRequest.getPassword() == null) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new JwtResponse("Username and password are required")); + } + + Authentication auth = new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), loginRequest.getPassword() + ); + + authenticationManager.authenticate(auth); + + UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername()); + String jwt = jwtUtil.generateToken(userDetails); + return ResponseEntity.ok(new JwtResponse(jwt)); + } catch (BadCredentialsException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new JwtResponse("Invalid username or password")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new JwtResponse("An unexpected error occurred")); + } + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Controllers/ClientController.java b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/ClientController.java new file mode 100644 index 0000000..d2f5893 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/ClientController.java @@ -0,0 +1,80 @@ +package Controllers; + +import Services.ClientService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/client") +public class ClientController { + + private final ClientService clientService; + + @Autowired + public ClientController(ClientService clientService) { + this.clientService = clientService; + } + + @PreAuthorize("hasRole('CLIENT')") + @GetMapping("/me") + public ResponseEntity getCurrentUser() { + return clientService.getCurrentUser(); + } + + @PreAuthorize("hasRole('CLIENT')") + @GetMapping("/accounts") + public ResponseEntity getMyAccounts() { + return clientService.getMyAccounts(); + } + + @PreAuthorize("hasRole('CLIENT')") + @GetMapping("/accounts/{id}") + public ResponseEntity getAccountById(@PathVariable int id) { + return clientService.getAccountById(id); + } + + @PreAuthorize("hasRole('CLIENT')") + @PostMapping("/friends/add/{otherId}") + public ResponseEntity addFriend(@PathVariable int otherId) { + return clientService.addFriend(otherId); + } + + @PreAuthorize("hasRole('CLIENT')") + @PostMapping("/friends/remove/{otherId}") + public ResponseEntity removeFriend(@PathVariable int otherId) { + return clientService.removeFriend(otherId); + } + + @PreAuthorize("hasRole('CLIENT')") + @PostMapping("/accounts/{accountId}/deposit/{amount}") + public ResponseEntity deposit(@PathVariable int accountId, @PathVariable double amount) { + return clientService.deposit(accountId, amount); + } + + @PreAuthorize("hasRole('CLIENT')") + @PostMapping("/accounts/{accountId}/withdraw/{amount}") + public ResponseEntity withdraw(@PathVariable int accountId, @PathVariable double amount) { + return clientService.withdraw(accountId, amount); + } + + @PreAuthorize("hasRole('CLIENT')") + @PostMapping("/accounts/transfer/{fromId}/{toId}/{amount}") + public ResponseEntity transfer( + @PathVariable int fromId, + @PathVariable int toId, + @PathVariable double amount + ) { + return clientService.transfer(fromId, toId, amount); + } + + @PreAuthorize("isAuthenticated()") + @PostMapping("/logout") + public ResponseEntity logout() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return ResponseEntity.ok("Client " + authentication.getName() + " logged out."); + } +} \ No newline at end of file diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Controllers/Handlers/GlobalExceptionHandler.java b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/Handlers/GlobalExceptionHandler.java new file mode 100644 index 0000000..c18c0f4 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Controllers/Handlers/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package Controllers.Handlers; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + e.printStackTrace(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error: " + e.getMessage()); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/DTO/BankAccountDTO.java b/ApiGateway/PresentationApiGateway/src/main/java/DTO/BankAccountDTO.java new file mode 100644 index 0000000..c795c80 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/DTO/BankAccountDTO.java @@ -0,0 +1,15 @@ +package DTO; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class BankAccountDTO { + private Integer id; + private Double balance; + private String userLogin; + private Integer userId; +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/DTO/OperationDTO.java b/ApiGateway/PresentationApiGateway/src/main/java/DTO/OperationDTO.java new file mode 100644 index 0000000..f9c1e1e --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/DTO/OperationDTO.java @@ -0,0 +1,15 @@ +package DTO; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class OperationDTO { + private Integer id; + private String type; + private Double amount; + private Integer accountId; +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/DTO/UserDTO.java b/ApiGateway/PresentationApiGateway/src/main/java/DTO/UserDTO.java new file mode 100644 index 0000000..fbe6357 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/DTO/UserDTO.java @@ -0,0 +1,21 @@ +package DTO; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class UserDTO { + private Integer id; + private String login; + private String name; + private Integer age; + private String sex; + private String hairType; + private List friendIds; + private List bankAccountIds; +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpAdminUtil.java b/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpAdminUtil.java new file mode 100644 index 0000000..65f829c --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpAdminUtil.java @@ -0,0 +1,26 @@ +package JWT; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class HttpAdminUtil { + private String getCurrentToken() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getCredentials() instanceof String token) { + return token; + } + return null; + } + + public HttpEntity withAuthHeaders() { + String token = getCurrentToken(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + return new HttpEntity<>(headers); + } +} + diff --git a/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpClientUtil.java b/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpClientUtil.java new file mode 100644 index 0000000..1fc2292 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/JWT/HttpClientUtil.java @@ -0,0 +1,32 @@ +package JWT; + +import Services.UserService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class HttpClientUtil { + private String getCurrentToken() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getCredentials() instanceof String token) { + return token; + } + return null; + } + + public Long getCurrentUserId(UserService userService) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + return userService.FindUserByUsername(username).getId(); + } + + public HttpEntity withAuthHeaders() { + String token = getCurrentToken(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + return new HttpEntity<>(headers); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtAuthFilter.java b/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtAuthFilter.java new file mode 100644 index 0000000..28bf617 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtAuthFilter.java @@ -0,0 +1,59 @@ +package JWT; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Autowired + public JwtAuthFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String path = request.getServletPath(); + if (path.startsWith("/auth") || path.equals("/login")) { + filterChain.doFilter(request, response); + return; + } + + String authHeader = request.getHeader("Authorization"); + String username = null; + String jwt = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + jwt = authHeader.substring(7); + username = jwtUtil.extractUsername(jwt); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (jwtUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtUtil.java b/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtUtil.java new file mode 100644 index 0000000..dcc0c2b --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/JWT/JwtUtil.java @@ -0,0 +1,82 @@ +package JWT; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + private SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token){ + return Jwts + .parser() + .verifyWith(getSecretKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put("role", userDetails.getAuthorities().stream() + .map(grantedAuthority -> grantedAuthority.getAuthority()) + .collect(Collectors.toList())); + + return createToken(claims, userDetails.getUsername()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSecretKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + public List extractRoles(String token) { + return extractClaim(token, claims -> claims.get("role", List.class)); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Requests/LoginRequest.java b/ApiGateway/PresentationApiGateway/src/main/java/Requests/LoginRequest.java new file mode 100644 index 0000000..2c4651b --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Requests/LoginRequest.java @@ -0,0 +1,24 @@ +package Requests; + +public class LoginRequest { + private String username; + private String password; + + public LoginRequest() {} + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserFilterRequest.java b/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserFilterRequest.java new file mode 100644 index 0000000..4b47a63 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserFilterRequest.java @@ -0,0 +1,9 @@ +package Requests; + +import lombok.Data; + +@Data +public class UserFilterRequest { + private String sex; + private String hairColor; +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserRequest.java b/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserRequest.java new file mode 100644 index 0000000..db2994f --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Requests/UserRequest.java @@ -0,0 +1,13 @@ +package Requests; + +import Models.Enums.Role; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserRequest { + private String username; + private String password; + private Role role; +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Responses/JwtResponse.java b/ApiGateway/PresentationApiGateway/src/main/java/Responses/JwtResponse.java new file mode 100644 index 0000000..3d5abad --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Responses/JwtResponse.java @@ -0,0 +1,17 @@ +package Responses; + +public class JwtResponse { + private String token; + + public JwtResponse(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Services/AdminService.java b/ApiGateway/PresentationApiGateway/src/main/java/Services/AdminService.java new file mode 100644 index 0000000..721f566 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Services/AdminService.java @@ -0,0 +1,91 @@ +package Services; + +import DTO.BankAccountDTO; +import DTO.OperationDTO; +import DTO.UserDTO; +import JWT.HttpAdminUtil; +import Requests.UserFilterRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.List; + +@Service +public class AdminService { + + private final RestTemplate restTemplate; + private final HttpAdminUtil adminUtil; + + @Autowired + public AdminService(RestTemplate restTemplate, HttpAdminUtil adminUtil) { + this.restTemplate = restTemplate; + this.adminUtil = adminUtil; + } + + public List getFilteredUsers(UserFilterRequest filterRequest) { + String url = "http://localhost:8080/data/users?sex=" + + (filterRequest.getSex() != null ? filterRequest.getSex() : "") + + "&hairColor=" + (filterRequest.getHairColor() != null ? filterRequest.getHairColor() : ""); + + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + adminUtil.withAuthHeaders(), + new ParameterizedTypeReference<>() {} + ); + return response.getBody(); + } + + public UserDTO getUserById(int id) { + String url = "http://localhost:8080/users/" + id; + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + adminUtil.withAuthHeaders(), + UserDTO.class + ); + return response.getBody(); + } + + public List getAllBankAccounts() { + String url = "http://localhost:8080/data/accounts"; + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + adminUtil.withAuthHeaders(), + new ParameterizedTypeReference<>() {} + ); + return response.getBody(); + } + + public List getAccountsByUserId(int userId) { + String url = "http://localhost:8080/data/users/" + userId + "/accounts"; + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + adminUtil.withAuthHeaders(), + BankAccountDTO[].class + ); + return Arrays.asList(response.getBody()); + } + + public List getOperationsByAccountId(int accountId, String type) { + String url = "http://localhost:8080/data/operations?accountId=" + accountId; + if (type != null && !type.isEmpty()) { + url += "&type=" + type; + } + + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + adminUtil.withAuthHeaders(), + new ParameterizedTypeReference<>() {} + ); + return response.getBody(); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/java/Services/ClientService.java b/ApiGateway/PresentationApiGateway/src/main/java/Services/ClientService.java new file mode 100644 index 0000000..2872e98 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/java/Services/ClientService.java @@ -0,0 +1,89 @@ +package Services; + +import DTO.BankAccountDTO; +import DTO.UserDTO; +import JWT.HttpClientUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Objects; + +@Service +public class ClientService { + + private final RestTemplate restTemplate; + private final HttpClientUtil clientUtil; + private final UserService userService; + + @Autowired + public ClientService(RestTemplate restTemplate, HttpClientUtil clientUtil, UserService userService) { + this.restTemplate = restTemplate; + this.clientUtil = clientUtil; + this.userService = userService; + } + + public ResponseEntity getCurrentUser() { + Long userId = clientUtil.getCurrentUserId(userService); + String url = "http://localhost:8080/users/" + userId; + return restTemplate.exchange(url, HttpMethod.GET, clientUtil.withAuthHeaders(), UserDTO.class); + } + + public ResponseEntity getMyAccounts() { + Long userId = clientUtil.getCurrentUserId(userService); + String url = "http://localhost:8080/data/users/" + userId + "/accounts"; + return restTemplate.exchange(url, HttpMethod.GET, clientUtil.withAuthHeaders(), BankAccountDTO[].class); + } + + public ResponseEntity getAccountById(int id) { + Long userId = clientUtil.getCurrentUserId(userService); + String userAccountsUrl = "http://localhost:8080/data/users/" + userId + "/accounts"; + ResponseEntity accountsResponse = restTemplate.exchange( + userAccountsUrl, HttpMethod.GET, clientUtil.withAuthHeaders(), BankAccountDTO[].class); + + boolean ownsAccount = false; + if (accountsResponse.getStatusCode().is2xxSuccessful()) { + for (BankAccountDTO account : Objects.requireNonNull(accountsResponse.getBody())) { + if (account.getId() == id) { + ownsAccount = true; + break; + } + } + } + + if (!ownsAccount) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied to account."); + } + + String url = "http://localhost:8080/data/accounts/" + id; + return restTemplate.exchange(url, HttpMethod.GET, clientUtil.withAuthHeaders(), BankAccountDTO.class); + } + + public ResponseEntity addFriend(int otherId) { + Long userId = clientUtil.getCurrentUserId(userService); + String url = "http://localhost:8080/interactions/friends/add/" + userId + "/" + otherId; + return restTemplate.exchange(url, HttpMethod.POST, clientUtil.withAuthHeaders(), String.class); + } + + public ResponseEntity removeFriend(int otherId) { + Long userId = clientUtil.getCurrentUserId(userService); + String url = "http://localhost:8080/interactions/friends/remove/" + userId + "/" + otherId; + return restTemplate.exchange(url, HttpMethod.POST, clientUtil.withAuthHeaders(), String.class); + } + + public ResponseEntity deposit(int accountId, double amount) { + String url = "http://localhost:8080/interactions/deposit/" + accountId + "/" + amount; + return restTemplate.exchange(url, HttpMethod.POST, clientUtil.withAuthHeaders(), String.class); + } + + public ResponseEntity withdraw(int accountId, double amount) { + String url = "http://localhost:8080/interactions/withdraw/" + accountId + "/" + amount; + return restTemplate.exchange(url, HttpMethod.POST, clientUtil.withAuthHeaders(), String.class); + } + + public ResponseEntity transfer(int fromId, int toId, double amount) { + String url = "http://localhost:8080/interactions/transfer/" + fromId + "/" + toId + "/" + amount; + return restTemplate.exchange(url, HttpMethod.POST, clientUtil.withAuthHeaders(), String.class); + } +} diff --git a/ApiGateway/PresentationApiGateway/src/main/resources/application.yml b/ApiGateway/PresentationApiGateway/src/main/resources/application.yml new file mode 100644 index 0000000..22e8c38 --- /dev/null +++ b/ApiGateway/PresentationApiGateway/src/main/resources/application.yml @@ -0,0 +1,40 @@ +spring: + application: + name: ApiGateway + + main: + allow-bean-definition-overriding: true + + flyway: + enabled: false + + datasource: + url: jdbc:postgresql://localhost:5432/JavaLabsApiDb + username: postgres + password: limosha + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: validate + show-sql: true + open-in-view: false + properties: + hibernate: + format_sql: true + + thymeleaf: + enabled: false + check-template-location: false + +# JWT Configuration +jwt: + secret: Z2FsdGZpZDpxNDqf9r1mPZYuZAsZZEwOovUEY9NhcY46uo3bB9XzRXMKbK6R03Qp + expiration: 86400000 # 1 day + +server: + port: 8081 + error: + include-message: always + include-binding-errors: always + include-stacktrace: never diff --git a/ApiGateway/pom.xml b/ApiGateway/pom.xml new file mode 100644 index 0000000..b64166c --- /dev/null +++ b/ApiGateway/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + com.example + lim0sha + 1.0-SNAPSHOT + + + ApiGateway + pom + + ApplicationApiGateway + PresentationApiGateway + + + + 21 + 21 + UTF-8 + + + + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + + + org.postgresql + postgresql + runtime + + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + + + io.github.cdimascio + dotenv-java + 2.2.4 + + + + + \ No newline at end of file diff --git a/Presentation/src/main/java/Presentation/Controllers/UserDTOController.java b/Presentation/src/main/java/Presentation/Controllers/UserDTOController.java index b008660..0faeafc 100644 --- a/Presentation/src/main/java/Presentation/Controllers/UserDTOController.java +++ b/Presentation/src/main/java/Presentation/Controllers/UserDTOController.java @@ -45,7 +45,7 @@ public ResponseEntity getUserById(@Parameter(description = "ID поль @ApiResponse(responseCode = "201", description = "Пользователь создан"), @ApiResponse(responseCode = "400", description = "Некорректные данные") }) - @PostMapping + @PostMapping("/create") public ResponseEntity createUser(@RequestBody User user) { UserResult result = baseController.CreateUser(user); if (result instanceof UserResult.Success) { @@ -61,7 +61,7 @@ public ResponseEntity createUser(@RequestBody User user) { @ApiResponse(responseCode = "404", description = "Пользователь не найден"), @ApiResponse(responseCode = "400", description = "Некорректные данные") }) - @PutMapping("/{id}") + @PutMapping("/update/{id}") public ResponseEntity updateUser(@Parameter(description = "ID пользователя") @PathVariable int id, @RequestBody User user) { User existingUser = baseController.GetUserById(id); if (existingUser == null) { @@ -80,7 +80,7 @@ public ResponseEntity updateUser(@Parameter(description = "ID пользов @ApiResponse(responseCode = "204", description = "Пользователь удален"), @ApiResponse(responseCode = "404", description = "Пользователь не найден") }) - @DeleteMapping("/{id}") + @DeleteMapping("/delete/{id}") public ResponseEntity deleteUser(@Parameter(description = "ID пользователя") @PathVariable int id) { User user = baseController.GetUserById(id); if (user == null) { diff --git a/Presentation/src/main/java/Presentation/Controllers/UserInteractionsController.java b/Presentation/src/main/java/Presentation/Controllers/UserInteractionsController.java new file mode 100644 index 0000000..53fe9be --- /dev/null +++ b/Presentation/src/main/java/Presentation/Controllers/UserInteractionsController.java @@ -0,0 +1,111 @@ +package Presentation.Controllers; + +import Application.Models.Entities.BankAccount; +import Application.ResultTypes.OperationResult; +import Presentation.Interfaces.IBaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/interactions") +public class UserInteractionsController { + + private final IBaseController baseController; + + @Autowired + public UserInteractionsController(IBaseController baseController) { + this.baseController = baseController; + } + + @Operation(summary = "Добавить друга", description = "Добавляет пользователя в друзья") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Друг добавлен"), + @ApiResponse(responseCode = "400", description = "Ошибка добавления") + }) + @PostMapping("/friends/add/{userId}/{otherId}") + public ResponseEntity addFriend( + @PathVariable int userId, + @PathVariable int otherId + ) { + baseController.AddFriend(userId, otherId); + return ResponseEntity.ok("Friend added."); + } + + @Operation(summary = "Удалить друга", description = "Удаляет пользователя из друзей") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Друг удалён"), + @ApiResponse(responseCode = "400", description = "Ошибка удаления") + }) + @PostMapping("/friends/remove/{userId}/{otherId}") + public ResponseEntity removeFriend( + @PathVariable int userId, + @PathVariable int otherId + ) { + baseController.RemoveFriend(userId, otherId); + return ResponseEntity.ok("Friend removed."); + } + + @Operation(summary = "Пополнить счёт", description = "Осуществляет пополнение счёта") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Пополнение выполнено"), + @ApiResponse(responseCode = "400", description = "Ошибка пополнения") + }) + @PostMapping("/deposit/{accountId}/{amount}") + public ResponseEntity deposit( + @PathVariable int accountId, + @PathVariable double amount + ) { + BankAccount account = baseController.GetBankAccountById(accountId); + OperationResult result = baseController.Deposit(account, amount); + + if (result instanceof OperationResult.Success) { + return ResponseEntity.ok("Deposit successful."); + } + return ResponseEntity.badRequest().body(result.toString()); + } + + @Operation(summary = "Снять со счёта", description = "Осуществляет снятие средств со счёта") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Снятие выполнено"), + @ApiResponse(responseCode = "400", description = "Ошибка снятия") + }) + @PostMapping("/withdraw/{accountId}/{amount}") + public ResponseEntity withdraw( + @PathVariable int accountId, + @PathVariable double amount + ) { + BankAccount account = baseController.GetBankAccountById(accountId); + OperationResult result = baseController.Withdraw(account, amount); + + if (result instanceof OperationResult.Success) { + return ResponseEntity.ok("Withdrawal successful."); + } + return ResponseEntity.badRequest().body(result.toString()); + } + + @Operation(summary = "Перевод между счетами", description = "Выполняет перевод между счетами с учётом комиссии") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Перевод выполнен"), + @ApiResponse(responseCode = "400", description = "Ошибка перевода") + }) + @PostMapping("/transfer/{fromAccountId}/{toAccountId}/{amount}") + public ResponseEntity transfer( + @PathVariable int fromAccountId, + @PathVariable int toAccountId, + @PathVariable double amount + ) { + BankAccount fromAccount = baseController.GetBankAccountById(fromAccountId); + BankAccount toAccount = baseController.GetBankAccountById(toAccountId); + + OperationResult result = baseController.Transfer(fromAccount, toAccount, amount); + + if (result instanceof OperationResult.Success) { + return ResponseEntity.ok("Transfer successful."); + } + return ResponseEntity.badRequest().body(result.toString()); + } +} diff --git a/pom.xml b/pom.xml index 3b6964c..b7f3d90 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,8 @@ Application DataAccess Presentation + ApiGateway + ApiGateway/DataAccessApiGateway