diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml
new file mode 100644
index 0000000..cfe6890
--- /dev/null
+++ b/.github/workflows/java.yml
@@ -0,0 +1,16 @@
+name: Java CI
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up JDK 21
+ uses: actions/setup-java@v2
+ with:
+ java-version: '23'
+ distribution: 'temurin'
+ - name: Build with Maven
+ run: mvn --batch-mode --update-snapshots package
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e8857c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,90 @@
+##############################
+## Java
+##############################
+.mtj.tmp/
+*.class
+*.jar
+*.war
+*.ear
+*.nar
+hs_err_pid*
+replay_pid*
+
+##############################
+## Maven
+##############################
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+pom.xml.bak
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+##############################
+## Gradle
+##############################
+bin/
+build/
+.gradle
+.gradletasknamecache
+gradle-app.setting
+!gradle-wrapper.jar
+
+##############################
+## IntelliJ
+##############################
+out/
+.idea/
+.idea_modules/
+*.iml
+*.ipr
+*.iws
+
+##############################
+## Eclipse
+##############################
+.settings/
+bin/
+tmp/
+.metadata
+.classpath
+.project
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.loadpath
+.factorypath
+
+##############################
+## NetBeans
+##############################
+nbproject/private/
+build/
+nbbuild/
+dist/
+nbdist/
+nbactions.xml
+nb-configuration.xml
+
+##############################
+## Visual Studio Code
+##############################
+.vscode/
+.code-workspace
+
+##############################
+## OS X
+##############################
+.DS_Store
+
+##############################
+## Miscellaneous
+##############################
+*.log
\ No newline at end of file
diff --git a/API-Gateway/pom.xml b/API-Gateway/pom.xml
new file mode 100644
index 0000000..ce3b36b
--- /dev/null
+++ b/API-Gateway/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+
+
+ API-Gateway
+
+
+ 23
+ 23
+ UTF-8
+
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.19.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.19.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.19.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ 3.4.4
+
+
+ org.bank
+ bank-infrastructure
+ 1.0-SNAPSHOT
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/API-Gateway/src/main/java/org/gateway/application/filters/JwtAuthFilter.java b/API-Gateway/src/main/java/org/gateway/application/filters/JwtAuthFilter.java
new file mode 100644
index 0000000..c618d6d
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/filters/JwtAuthFilter.java
@@ -0,0 +1,64 @@
+package org.gateway.application.filters;
+
+import org.gateway.application.services.JwtServices;
+import org.gateway.application.services.UserDetailsServiceImpl;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.lang.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtServices jwtService;
+ private final UserDetailsServiceImpl userDetailsService;
+
+ @Override
+ protected void doFilterInternal(@NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain chain)
+ throws IOException, ServletException {
+ final String authHeader = extractToken(request);
+ final String jwt;
+ final String login;
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ jwt = authHeader.substring(7);
+ login = jwtService.extractLogin(jwt);
+
+ if (login != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails = userDetailsService.loadUserByUsername(login);
+
+ if (jwtService.isTokenValid(jwt, userDetails)) {
+ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
+ userDetails, jwt,
+ userDetails.getAuthorities());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ private String extractToken(HttpServletRequest request) {
+ String header = request.getHeader("Authorization");
+ return header != null && header.startsWith("Bearer ") ? header : null;
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/interfaces/AdminApi.java b/API-Gateway/src/main/java/org/gateway/application/interfaces/AdminApi.java
new file mode 100644
index 0000000..5815e4c
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/interfaces/AdminApi.java
@@ -0,0 +1,27 @@
+package org.gateway.application.interfaces;
+
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import org.gateway.infrastructure.requestEntities.CreateUserRequest;
+import org.springframework.security.core.Authentication;
+
+import java.util.List;
+
+public interface AdminApi {
+
+ void createClient(CreateUserRequest clientRequest, String password, Authentication auth);
+
+ List getAllUsers(Authentication auth);
+
+ List getAllUsersGenderFilter(String gender, Authentication auth);
+
+ List getAllUsersHairColorFilter(String hairColor, Authentication auth);
+
+ GatewayUserDTO getUserById(long id, Authentication auth);
+
+ List getAllAccounts(Authentication auth);
+
+ List getAllUserAccounts(long id, Authentication auth);
+
+ GatewayAccountDTO getAccountById(long id, Authentication auth);
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/interfaces/ClientApi.java b/API-Gateway/src/main/java/org/gateway/application/interfaces/ClientApi.java
new file mode 100644
index 0000000..3e8011a
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/interfaces/ClientApi.java
@@ -0,0 +1,29 @@
+package org.gateway.application.interfaces;
+
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayFriendsAccountsDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import org.gateway.infrastructure.requestEntities.TransferRequest;
+import org.springframework.security.core.Authentication;
+
+import java.util.List;
+
+public interface ClientApi {
+ GatewayUserDTO getSelf(Authentication auth);
+
+ List getMyAccounts(Authentication auth);
+
+ GatewayAccountDTO getAccountById(Long id, Authentication auth);
+
+ List getFriendsAccounts(Authentication auth);
+
+ void transfer(TransferRequest transferRequest, Authentication auth);
+
+ void addFriend(Long friendId, Authentication auth);
+
+ void removeFriend(Long friendId, Authentication auth);
+
+ void deposit(Long id, double amount, Authentication auth);
+
+ void withdraw(Long id, double amount, Authentication auth);
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/services/AdminServices.java b/API-Gateway/src/main/java/org/gateway/application/services/AdminServices.java
new file mode 100644
index 0000000..c59e724
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/services/AdminServices.java
@@ -0,0 +1,68 @@
+package org.gateway.application.services;
+
+import org.gateway.application.interfaces.AdminApi;
+import org.gateway.infrastructure.entities.GatewayUser;
+import org.gateway.infrastructure.entities.enums.Role;
+import org.gateway.infrastructure.repos.GatewayUserRepository;
+import org.gateway.infrastructure.requestEntities.CreateAdminRequest;
+import org.gateway.infrastructure.requestEntities.CreateUserRequest;
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class AdminServices {
+ private final GatewayUserRepository gatewayUserRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ private final AdminApi adminApi;
+
+ @Transactional
+ public void createAdmin(CreateAdminRequest admin) {
+ GatewayUser gatewayUser = new GatewayUser(admin.getLogin(),
+ passwordEncoder.encode(admin.getPassword()), List.of(Role.ADMIN));
+ gatewayUserRepository.save(gatewayUser);
+ }
+
+ @Transactional
+ public void createClient(CreateUserRequest clientRequest, String password, Authentication auth) {
+ GatewayUser entity = new GatewayUser(clientRequest.getLogin(),
+ passwordEncoder.encode(password), List.of(Role.CLIENT));
+ gatewayUserRepository.save(entity);
+ adminApi.createClient(clientRequest, password, auth);
+ }
+
+ public List getAllUsers(Authentication auth) {
+ return adminApi.getAllUsers(auth);
+ }
+
+ public List getAllUsersGenderFilter(String gender, Authentication auth) {
+ return adminApi.getAllUsersGenderFilter(gender, auth);
+ }
+
+ public List getAllUsersHairColorFilter(String haircolor, Authentication auth) {
+ return adminApi.getAllUsersHairColorFilter(haircolor, auth);
+ }
+
+ public GatewayUserDTO getUserById(long id, Authentication auth) {
+ return adminApi.getUserById(id, auth);
+ }
+
+ public List getAllAccounts(Authentication auth) {
+ return adminApi.getAllAccounts(auth);
+ }
+
+ public List getAllUserAccounts(long user_id, Authentication auth) {
+ return adminApi.getAllUserAccounts(user_id, auth);
+ }
+
+ public GatewayAccountDTO getAccountById(long accountId, Authentication auth) {
+ return adminApi.getAccountById(accountId, auth);
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/services/AuthService.java b/API-Gateway/src/main/java/org/gateway/application/services/AuthService.java
new file mode 100644
index 0000000..1033768
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/services/AuthService.java
@@ -0,0 +1,60 @@
+package org.gateway.application.services;
+
+import org.gateway.infrastructure.entities.GatewayUser;
+import org.gateway.infrastructure.entities.TokenBlacklist;
+import org.gateway.infrastructure.repos.GatewayUserRepository;
+import org.gateway.infrastructure.repos.TokenBlackListRepository;
+import org.gateway.infrastructure.requestEntities.LoginRequest;
+import org.gateway.infrastructure.DTO.LoginDTO;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDeniedException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Instant;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+ private final GatewayUserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtServices jwtService;
+ private final TokenBlackListRepository tokenBlacklistRepository;
+ private final AuthenticationManager authenticationManager;
+
+
+ public LoginDTO login(LoginRequest loginRequest) {
+ GatewayUser user = userRepository.findByLogin(loginRequest.getLogin());
+ if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
+ throw new AuthorizationDeniedException("Invalid login or password");
+ }
+
+ String token = authenticateAndGenerateToken(loginRequest.getLogin(), loginRequest.getPassword());
+ return new LoginDTO(token, user.getLogin(), user.getRole());
+ }
+
+ @Transactional
+ public void logout(HttpServletRequest request) {
+ String token = request.getHeader("Authorization");
+ if (!tokenBlacklistRepository.existsByToken(token)) {
+ TokenBlacklist blacklistedToken = new TokenBlacklist();
+ blacklistedToken.setToken(token);
+ blacklistedToken.setBlackListedAt(Instant.now());
+ tokenBlacklistRepository.save(blacklistedToken);
+ }
+ }
+
+ public String authenticateAndGenerateToken(String login, String password) {
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(login, password)
+ );
+ UserDetails user = (UserDetails) authentication.getPrincipal();
+ return jwtService.generateToken(user);
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/services/ClientServices.java b/API-Gateway/src/main/java/org/gateway/application/services/ClientServices.java
new file mode 100644
index 0000000..3068695
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/services/ClientServices.java
@@ -0,0 +1,77 @@
+package org.gateway.application.services;
+
+import org.gateway.application.interfaces.ClientApi;
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayFriendsAccountsDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import org.gateway.infrastructure.requestEntities.TransferRequest;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ClientServices {
+
+ private final ClientApi clientApi;
+
+ public GatewayUserDTO getSelf(Authentication auth) {
+ return clientApi.getSelf(auth);
+ }
+
+ public List getMyAccounts(Authentication auth) {
+ return clientApi.getMyAccounts(auth);
+ }
+
+ public GatewayAccountDTO getAccountById(long id, Authentication auth) {
+ return clientApi.getAccountById(id, auth);
+ }
+
+ @Transactional
+ public void addFriend(long id, Authentication auth) {
+ clientApi.addFriend(id, auth);
+ }
+
+ @Transactional
+ public void deleteFriend(long id, Authentication auth) {
+ clientApi.removeFriend(id, auth);
+ }
+
+ @Transactional
+ public void transfer(TransferRequest transferRequest, Authentication auth) {
+ clientApi.transfer(transferRequest, auth);
+ }
+
+
+ @Transactional
+ public void deposit(long id, double amount, Authentication auth) {
+ Long userId = getSelf(auth).getId();
+
+ Long ownerId = getAccountById(id, auth).getUserId();
+
+ if (!userId.equals(ownerId)) {
+ throw new IllegalArgumentException("You are not the owner of this account");
+ }
+
+ clientApi.deposit(id, amount, auth);
+ }
+
+
+ @Transactional
+ public void withdraw(long id, double amount, Authentication auth) {
+ Long userId = getSelf(auth).getId();
+ Long ownerId = getAccountById(id, auth).getUserId();
+ if (!userId.equals(ownerId)) {
+ throw new IllegalArgumentException("You are not the owner of this account");
+ }
+
+ clientApi.withdraw(id, amount, auth);
+ }
+
+ public List getFriendsAndAccounts(Authentication auth) {
+ return clientApi.getFriendsAccounts(auth);
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/application/services/JwtServices.java b/API-Gateway/src/main/java/org/gateway/application/services/JwtServices.java
new file mode 100644
index 0000000..86b2434
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/services/JwtServices.java
@@ -0,0 +1,77 @@
+package org.gateway.application.services;
+
+import org.gateway.config.JwtProperties;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+
+@Service
+@RequiredArgsConstructor
+public class JwtServices {
+
+ private final JwtProperties jwtProperties;
+
+ private SecretKey getSigningKey() {
+ String secret = jwtProperties.getSecret();
+ byte[] keyBytes = Base64.getEncoder().encode(secret.getBytes());
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ public String extractLogin(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ public String generateToken(UserDetails userDetails) {
+ Map extraClaims = new HashMap<>();
+ extraClaims.put("role", userDetails.getAuthorities().iterator().next().getAuthority());
+
+ return buildToken(extraClaims, userDetails);
+ }
+
+ private String buildToken(Map extraClaims, UserDetails userDetails) {
+ return Jwts.builder()
+ .claims(extraClaims)
+ .subject(userDetails.getUsername())
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(jwtProperties.getExpiration() * 1000 + System.currentTimeMillis()))
+ .signWith(getSigningKey(), Jwts.SIG.HS256)
+ .compact();
+ }
+
+ public boolean isTokenValid(String token, UserDetails userDetails) {
+ final String login = extractLogin(token);
+ return (login.equals(userDetails.getUsername()) && !isTokenExpired(token));
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ private Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+}
\ No newline at end of file
diff --git a/API-Gateway/src/main/java/org/gateway/application/services/UserDetailsServiceImpl.java b/API-Gateway/src/main/java/org/gateway/application/services/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..5ea659d
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/application/services/UserDetailsServiceImpl.java
@@ -0,0 +1,31 @@
+package org.gateway.application.services;
+
+import org.gateway.infrastructure.entities.GatewayUser;
+import org.gateway.infrastructure.repos.GatewayUserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+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
+@RequiredArgsConstructor
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ private final GatewayUserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String login) {
+ GatewayUser user = userRepository.findByLogin(login);
+ if (user == null) {
+ throw new UsernameNotFoundException("User not found with login: " + login);
+ }
+
+ return new User(user.getLogin(), user.getPassword(),
+ user.getRole().stream().map(role -> new SimpleGrantedAuthority(role.name())).toList());
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/config/AuthManagerConfig.java b/API-Gateway/src/main/java/org/gateway/config/AuthManagerConfig.java
new file mode 100644
index 0000000..9e56d6b
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/config/AuthManagerConfig.java
@@ -0,0 +1,15 @@
+package org.gateway.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+
+@Configuration
+public class AuthManagerConfig {
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+ return configuration.getAuthenticationManager();
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/config/BankApiConfig.java b/API-Gateway/src/main/java/org/gateway/config/BankApiConfig.java
new file mode 100644
index 0000000..24a2175
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/config/BankApiConfig.java
@@ -0,0 +1,27 @@
+package org.gateway.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Setter
+@Getter
+@Configuration
+@ConfigurationProperties(prefix = "bank.api")
+public class BankApiConfig {
+ private String url;
+ private String host;
+ private int port;
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
+ public String getBaseUrl() {
+ return "http://localhost:8081";
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/config/JwtProperties.java b/API-Gateway/src/main/java/org/gateway/config/JwtProperties.java
new file mode 100644
index 0000000..d60c6aa
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/config/JwtProperties.java
@@ -0,0 +1,15 @@
+package org.gateway.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties(prefix = "jwt")
+public class JwtProperties {
+ private String secret;
+ private long expiration;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/config/SecurityConfig.java b/API-Gateway/src/main/java/org/gateway/config/SecurityConfig.java
new file mode 100644
index 0000000..5083bee
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/config/SecurityConfig.java
@@ -0,0 +1,59 @@
+package org.gateway.config;
+
+import org.gateway.application.filters.JwtAuthFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+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;
+
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthFilter jwtAuthenticationFilter;
+
+ @Order(1)
+ @Bean
+ public SecurityFilterChain securityFilterChainForAuthentication(HttpSecurity http) throws Exception {
+ return getHttpBasicConfiguration(http)
+ .securityMatcher("api/v1/login")
+ .build();
+ }
+
+ @Order(2)
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return getHttpBasicConfiguration(http)
+ .securityMatcher("api/**")
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ .build();
+ }
+
+ public HttpSecurity getHttpBasicConfiguration(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(sessionManagement -> sessionManagement
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(configurer -> configurer
+ .requestMatchers("/api/auth/login").permitAll()
+ .anyRequest().authenticated()
+ );
+ return http;
+ }
+
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayAccountDTO.java b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayAccountDTO.java
new file mode 100644
index 0000000..6d5cae8
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayAccountDTO.java
@@ -0,0 +1,13 @@
+package org.gateway.infrastructure.DTO;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class GatewayAccountDTO {
+ private long id;
+ private Double balance;
+ private long userId;
+ private List transactions;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayFriendsAccountsDTO.java b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayFriendsAccountsDTO.java
new file mode 100644
index 0000000..58102a3
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayFriendsAccountsDTO.java
@@ -0,0 +1,16 @@
+package org.gateway.infrastructure.DTO;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class GatewayFriendsAccountsDTO {
+ private String name;
+ private List friendsAccounts;
+
+ public GatewayFriendsAccountsDTO(String name, List friendsAccounts) {
+ this.name = name;
+ this.friendsAccounts = friendsAccounts;
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayTransactionDTO.java b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayTransactionDTO.java
new file mode 100644
index 0000000..c4ffc22
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayTransactionDTO.java
@@ -0,0 +1,24 @@
+package org.gateway.infrastructure.DTO;
+
+import org.gateway.infrastructure.entities.enums.TransactionStatus;
+import org.gateway.infrastructure.entities.enums.TransactionTypes;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class GatewayTransactionDTO {
+ private Long id;
+ private TransactionTypes type;
+ private TransactionStatus status;
+ private String description;
+ private double amount;
+
+ public GatewayTransactionDTO(Long id, TransactionTypes type, TransactionStatus status, String description, double amount) {
+ this.id = id;
+ this.type = type;
+ this.status = status;
+ this.description = description;
+ this.amount = amount;
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayUserDTO.java b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayUserDTO.java
new file mode 100644
index 0000000..a2aadde
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/GatewayUserDTO.java
@@ -0,0 +1,11 @@
+package org.gateway.infrastructure.DTO;
+
+import lombok.Data;
+
+@Data
+public class GatewayUserDTO {
+ private Long id;
+ private String Name;
+ private String gender;
+ private String hairColor;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/LoginDTO.java b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/LoginDTO.java
new file mode 100644
index 0000000..e341183
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/DTO/LoginDTO.java
@@ -0,0 +1,15 @@
+package org.gateway.infrastructure.DTO;
+
+import org.gateway.infrastructure.entities.enums.Role;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class LoginDTO {
+ private String token;
+ private String login;
+ private List role;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/UserPrincipalImpl.java b/API-Gateway/src/main/java/org/gateway/infrastructure/UserPrincipalImpl.java
new file mode 100644
index 0000000..609a30d
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/UserPrincipalImpl.java
@@ -0,0 +1,47 @@
+package org.gateway.infrastructure;
+
+import org.gateway.infrastructure.entities.enums.Role;
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+
+@Getter
+@Builder
+public class UserPrincipalImpl implements UserDetails {
+ private final long id;
+ private final String username;
+ private final String password;
+ private final Role role;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority(role.name()));
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/entities/GatewayUser.java b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/GatewayUser.java
new file mode 100644
index 0000000..4062d7c
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/GatewayUser.java
@@ -0,0 +1,33 @@
+package org.gateway.infrastructure.entities;
+
+
+import org.gateway.infrastructure.entities.enums.Role;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.util.List;
+
+@Entity
+@Table(name = "gateway_users")
+@Data
+@NoArgsConstructor
+public class GatewayUser {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String login;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Enumerated(EnumType.STRING)
+ private List role;
+
+ public GatewayUser(String login, String encode, List admin) {
+ this.login = login;
+ this.password = encode;
+ this.role = admin;
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/entities/TokenBlacklist.java b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/TokenBlacklist.java
new file mode 100644
index 0000000..2488611
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/TokenBlacklist.java
@@ -0,0 +1,23 @@
+package org.gateway.infrastructure.entities;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "token_blacklist")
+public class TokenBlacklist {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String token;
+
+ private Instant blackListedAt;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/Role.java b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/Role.java
new file mode 100644
index 0000000..1808d42
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/Role.java
@@ -0,0 +1,9 @@
+package org.gateway.infrastructure.entities.enums;
+
+public enum Role {
+ ADMIN,
+ CLIENT
+}
+
+
+
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionStatus.java b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionStatus.java
new file mode 100644
index 0000000..a4aad64
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionStatus.java
@@ -0,0 +1,7 @@
+package org.gateway.infrastructure.entities.enums;
+
+public enum TransactionStatus {
+ PENDING,
+ SUCCESS,
+ FAILED
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionTypes.java b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionTypes.java
new file mode 100644
index 0000000..e835489
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/entities/enums/TransactionTypes.java
@@ -0,0 +1,8 @@
+package org.gateway.infrastructure.entities.enums;
+
+public enum TransactionTypes {
+ DEPOSIT,
+ WITHDRAW,
+ TRANSFER;
+}
+
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/repos/GatewayUserRepository.java b/API-Gateway/src/main/java/org/gateway/infrastructure/repos/GatewayUserRepository.java
new file mode 100644
index 0000000..c0bdd7f
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/repos/GatewayUserRepository.java
@@ -0,0 +1,8 @@
+package org.gateway.infrastructure.repos;
+
+import org.gateway.infrastructure.entities.GatewayUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface GatewayUserRepository extends JpaRepository {
+ GatewayUser findByLogin(String login);
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/repos/TokenBlackListRepository.java b/API-Gateway/src/main/java/org/gateway/infrastructure/repos/TokenBlackListRepository.java
new file mode 100644
index 0000000..8a2ef59
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/repos/TokenBlackListRepository.java
@@ -0,0 +1,9 @@
+package org.gateway.infrastructure.repos;
+
+import org.gateway.infrastructure.entities.TokenBlacklist;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TokenBlackListRepository extends JpaRepository {
+ TokenBlacklist findByToken(String token);
+ boolean existsByToken(String token);
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateAdminRequest.java b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateAdminRequest.java
new file mode 100644
index 0000000..90883ae
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateAdminRequest.java
@@ -0,0 +1,9 @@
+package org.gateway.infrastructure.requestEntities;
+
+import lombok.Data;
+
+@Data
+public class CreateAdminRequest {
+ private String login;
+ private String password;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateUserRequest.java b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateUserRequest.java
new file mode 100644
index 0000000..4eedf47
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/CreateUserRequest.java
@@ -0,0 +1,13 @@
+package org.gateway.infrastructure.requestEntities;
+
+import lombok.Data;
+
+
+@Data
+public class CreateUserRequest {
+ private String login;
+ private String name;
+ private int age;
+ private String gender;
+ private String hairColor;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/LoginRequest.java b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/LoginRequest.java
new file mode 100644
index 0000000..0d8197c
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/LoginRequest.java
@@ -0,0 +1,9 @@
+package org.gateway.infrastructure.requestEntities;
+
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+ private String login;
+ private String password;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/TransferRequest.java b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/TransferRequest.java
new file mode 100644
index 0000000..aefd8ff
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/infrastructure/requestEntities/TransferRequest.java
@@ -0,0 +1,14 @@
+package org.gateway.infrastructure.requestEntities;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class TransferRequest {
+ private Long fromAccountId;
+ private Long toAccountId;
+ private double amount;
+}
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/AdminApiImpl.java b/API-Gateway/src/main/java/org/gateway/presentation/AdminApiImpl.java
new file mode 100644
index 0000000..8233131
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/AdminApiImpl.java
@@ -0,0 +1,164 @@
+package org.gateway.presentation;
+
+import org.gateway.application.interfaces.AdminApi;
+import org.gateway.config.BankApiConfig;
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import org.gateway.infrastructure.requestEntities.CreateUserRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class AdminApiImpl implements AdminApi {
+
+ private final RestTemplate restTemplate;
+ private final BankApiConfig bankApiConfig;
+
+ @Override
+ public void createClient(CreateUserRequest clientRequest, String password, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ HttpEntity requestEntity = new HttpEntity<>(clientRequest, headers);
+ restTemplate.postForEntity(bankApiConfig.getUrl() + "/api/users", requestEntity, CreateUserRequest.class);
+ }
+
+ @Override
+ public List getAllUsers(Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ String url = bankApiConfig.getUrl() + "/api/users";
+
+
+ ResponseEntity response = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ entity,
+ GatewayUserDTO[].class
+ );
+
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return Arrays.asList(response.getBody());
+ }
+
+ @Override
+ public List getAllUsersGenderFilter(String gender, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ String url = bankApiConfig.getUrl() + "/api/users/{gender}/";
+ ResponseEntity response = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ entity,
+ GatewayUserDTO[].class,
+ gender);
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return Arrays.asList(response.getBody());
+ }
+
+ @Override
+ public List getAllUsersHairColorFilter(String haircolor, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ String url = bankApiConfig.getUrl() + "/api/users/{haircolor}";
+ ResponseEntity response = restTemplate.exchange(
+ url,
+ HttpMethod.GET,
+ entity,
+ GatewayUserDTO[].class,
+ haircolor
+ );
+
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return Arrays.asList(response.getBody());
+ }
+
+ @Override
+ public GatewayUserDTO getUserById(long id, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{id}",
+ HttpMethod.GET,
+ entity,
+ GatewayUserDTO.class,
+ id
+ );
+
+ return response.getBody();
+ }
+
+ @Override
+ public List getAllAccounts(Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/accounts",
+ HttpMethod.GET,
+ entity,
+ GatewayAccountDTO[].class
+ );
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return Arrays.asList(response.getBody());
+ }
+
+ @Override
+ public List getAllUserAccounts(long id, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{id}/accounts",
+ HttpMethod.GET,
+ entity,
+ GatewayAccountDTO[].class,
+ id
+ );
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return Arrays.asList(response.getBody());
+ }
+
+ @Override
+ public GatewayAccountDTO getAccountById(long accountId, Authentication auth){
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ ResponseEntity accountResponse = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/accounts/{accountId}",
+ HttpMethod.GET,
+ entity,
+ GatewayAccountDTO.class,
+ accountId
+ );
+
+ return accountResponse.getBody();
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/ClientApiImpl.java b/API-Gateway/src/main/java/org/gateway/presentation/ClientApiImpl.java
new file mode 100644
index 0000000..6dbc15e
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/ClientApiImpl.java
@@ -0,0 +1,186 @@
+package org.gateway.presentation;
+
+import org.gateway.application.interfaces.ClientApi;
+import org.gateway.config.BankApiConfig;
+import org.gateway.infrastructure.DTO.GatewayAccountDTO;
+import org.gateway.infrastructure.DTO.GatewayFriendsAccountsDTO;
+import org.gateway.infrastructure.DTO.GatewayUserDTO;
+import org.gateway.infrastructure.requestEntities.TransferRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class ClientApiImpl implements ClientApi {
+
+ private final RestTemplate restTemplate;
+ private final BankApiConfig bankApiConfig;
+
+ private HttpEntity buildEntityWithAuth(Authentication auth) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(auth.getCredentials().toString());
+ return new HttpEntity<>(headers);
+ }
+
+ @Override
+ public GatewayUserDTO getSelf(Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ String login = auth.getName();
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{login}",
+ HttpMethod.GET,
+ entity,
+ GatewayUserDTO.class,
+ login
+ );
+
+ return response.getBody();
+ }
+
+ @Override
+ public List getMyAccounts(Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userId}/accounts",
+ HttpMethod.GET,
+ entity,
+ GatewayAccountDTO[].class,
+ userId
+ );
+
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return List.of(response.getBody());
+ }
+
+ @Override
+ public GatewayAccountDTO getAccountById(Long id, Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userId}/accounts/{id}",
+ HttpMethod.GET,
+ entity,
+ GatewayAccountDTO.class,
+ userId,
+ id
+ );
+
+ return response.getBody();
+ }
+
+ @Override
+ public List getFriendsAccounts(Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+
+ ResponseEntity response = restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userId}/friends",
+ HttpMethod.GET,
+ entity,
+ GatewayFriendsAccountsDTO[].class,
+ userId
+ );
+
+ if (response.getBody() == null) {
+ return List.of();
+ }
+ return List.of(response.getBody());
+ }
+
+ @Override
+ public void transfer(TransferRequest transferRequest, Authentication auth) {
+ HttpEntity entity = new HttpEntity<>(transferRequest, buildEntityWithAuth(auth).getHeaders());
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+
+ restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userId}/transfer",
+ HttpMethod.POST,
+ entity,
+ Void.class,
+ userId
+ );
+ }
+
+ @Override
+ public void addFriend(Long friendId, Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+
+ restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userID}/add_friend/{friendId}",
+ HttpMethod.POST,
+ entity,
+ Void.class,
+ userId,
+ friendId
+ );
+ }
+
+ @Override
+ public void removeFriend(Long friendId, Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ GatewayUserDTO user = getSelf(auth);
+ Long userId = user.getId();
+
+ restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/users/{userID}/delete_friend/{friendId}",
+ HttpMethod.DELETE,
+ entity,
+ Void.class,
+ userId,
+ friendId
+ );
+ }
+
+ @Override
+ public void deposit(Long id, double amount, Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ Map uriVariables = new HashMap<>();
+ uriVariables.put("id", id);
+ uriVariables.put("amount", amount);
+
+ restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/accounts/{id}/deposit?amount={amount}",
+ HttpMethod.POST,
+ entity,
+ Void.class,
+ uriVariables
+ );
+ }
+
+ @Override
+ public void withdraw(Long id, double amount, Authentication auth) {
+ HttpEntity entity = buildEntityWithAuth(auth);
+ Map uriVariables = new HashMap<>();
+ uriVariables.put("id", id);
+ uriVariables.put("amount", amount);
+
+ restTemplate.exchange(
+ bankApiConfig.getUrl() + "/api/accounts/{id}/withdraw?amount={amount}",
+ HttpMethod.POST,
+ entity,
+ Void.class,
+ uriVariables
+ );
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/GatewayApplication.java b/API-Gateway/src/main/java/org/gateway/presentation/GatewayApplication.java
new file mode 100644
index 0000000..fba2e47
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/GatewayApplication.java
@@ -0,0 +1,22 @@
+package org.gateway.presentation;
+
+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 = {
+ "org/gateway/presentation",
+ "org/gateway/application",
+ "org/gateway/infrastructure",
+ "org/gateway/config"
+})
+@EnableJpaRepositories(basePackages = "infrastructure.repos")
+@EntityScan(basePackages = {
+ "infrastructure.entities"
+})
+public class GatewayApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(GatewayApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/controllers/AdminController.java b/API-Gateway/src/main/java/org/gateway/presentation/controllers/AdminController.java
new file mode 100644
index 0000000..300fe31
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/controllers/AdminController.java
@@ -0,0 +1,111 @@
+package org.gateway.presentation.controllers;
+
+import org.gateway.application.services.AdminServices;
+import org.gateway.infrastructure.requestEntities.CreateAdminRequest;
+import org.gateway.infrastructure.requestEntities.CreateUserRequest;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/admin")
+@PreAuthorize("hasRole('ADMIN')")
+@RequiredArgsConstructor
+public class AdminController {
+ private final AdminServices adminService;
+
+ @Operation(summary = "Создать нового администратора")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Администратор успешно создан")
+ })
+ @PostMapping("/create-admin")
+ public ResponseEntity> createAdmin(@RequestBody CreateAdminRequest admin) {
+ adminService.createAdmin(admin);
+ return ResponseEntity.ok("Admin created successfully");
+ }
+
+ @Operation(summary = "Создать нового клиента")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Клиент успешно создан")
+ })
+ @PostMapping("/create-client")
+ public ResponseEntity> createClient(@RequestBody CreateUserRequest client,
+ @RequestParam("password") String password,
+ Authentication auth) {
+ adminService.createClient(client, password, auth);
+ return ResponseEntity.ok("Client created successfully");
+ }
+
+ @Operation(summary = "Получить пользователей с фильтрами по полу и цвету волос")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Список пользователей получен"),
+ @ApiResponse(responseCode = "404", description = "Пользователи не найдены")
+ })
+ @GetMapping("/all-users")
+ public ResponseEntity> getAllUsers(Authentication auth) {
+ return ResponseEntity.ok(adminService.getAllUsers(auth));
+ }
+
+ @Operation(summary = "Получить пользователей с фильтром по полу")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Список пользователей получен")
+ })
+ @GetMapping("/all-users/{gender}")
+ public ResponseEntity> getAllUsersFilteredByGender(@PathVariable("gender") String gender,
+ Authentication auth) {
+ return ResponseEntity.ok(adminService.getAllUsersGenderFilter(gender, auth));
+ }
+
+ @Operation(summary = "Получить пользователей с фильтром по цвету волос")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Список пользователей получен")
+ })
+ @GetMapping("/all-users/{hairColor}")
+ public ResponseEntity> getAllUsersFilteredByHairColor(@PathVariable("hairColor") String hairColor,
+ Authentication auth) {
+ return ResponseEntity.ok(adminService.getAllUsersHairColorFilter(hairColor, auth));
+ }
+
+ @Operation(summary = "Получить пользователя по ID")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Пользователь найден"),
+ @ApiResponse(responseCode = "404", description = "Пользователь не найден")
+ })
+ @GetMapping("/user/{id}")
+ public ResponseEntity> getUserById(@PathVariable("id") long id, Authentication auth) {
+ return ResponseEntity.ok(adminService.getUserById(id, auth));
+ }
+
+ @Operation(summary = "Получить всех пользователей")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Список пользователей получен")
+ })
+ @GetMapping("/all-accounts")
+ public ResponseEntity> getAllAccounts(Authentication authentication) {
+ return ResponseEntity.ok(adminService.getAllAccounts(authentication));
+ }
+
+ @Operation(summary = "Получить все счета конкретного пользователя")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Список аккаунтов пользователя получен")
+ })
+ @GetMapping("/users/{id}/accounts")
+ public ResponseEntity> getUserAccounts(@PathVariable("id") long id, Authentication authentication) {
+ return ResponseEntity.ok(adminService.getAllUserAccounts(id, authentication));
+ }
+
+ @Operation(summary = "Получить счёт по ID")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Счёт найден"),
+ @ApiResponse(responseCode = "404", description = "Счёт не найден")
+ })
+ @GetMapping("/accounts/{id}")
+ public ResponseEntity> getAccountById(@PathVariable("id") long id, Authentication authentication) {
+ return ResponseEntity.ok(adminService.getAccountById(id, authentication));
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/controllers/AuthController.java b/API-Gateway/src/main/java/org/gateway/presentation/controllers/AuthController.java
new file mode 100644
index 0000000..2a3cb18
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/controllers/AuthController.java
@@ -0,0 +1,45 @@
+package org.gateway.presentation.controllers;
+
+import org.gateway.application.services.AuthService;
+import org.gateway.infrastructure.requestEntities.LoginRequest;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+
+ @Operation(summary = "Аутентификация пользователя", description = "Позволяет пользователю войти в систему и получить JWT токен.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Успешный вход"),
+ @ApiResponse(responseCode = "401", description = "Неверные учетные данные")
+ })
+ @PostMapping("/login")
+ public ResponseEntity> login(@RequestBody LoginRequest request) {
+ return ResponseEntity.ok(authService.login(request));
+ }
+
+ @Operation(summary = "Выход пользователя", description = "Позволяет пользователю выйти из системы.")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Успешный выход"),
+ @ApiResponse(responseCode = "401", description = "Пользователь не авторизован")
+ })
+ @PostMapping("/logout")
+ @PreAuthorize("hasAnyRole('ADMIN', 'CLIENT')")
+ public ResponseEntity> logout(HttpServletRequest request) {
+ authService.logout(request);
+ return ResponseEntity.ok().build();
+ }
+}
diff --git a/API-Gateway/src/main/java/org/gateway/presentation/controllers/ClientController.java b/API-Gateway/src/main/java/org/gateway/presentation/controllers/ClientController.java
new file mode 100644
index 0000000..9625ee9
--- /dev/null
+++ b/API-Gateway/src/main/java/org/gateway/presentation/controllers/ClientController.java
@@ -0,0 +1,87 @@
+package org.gateway.presentation.controllers;
+
+import org.gateway.application.services.ClientServices;
+import org.gateway.infrastructure.requestEntities.TransferRequest;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/client")
+@PreAuthorize("hasRole('CLIENT')")
+@RequiredArgsConstructor
+public class ClientController {
+ private final ClientServices clientService;
+
+ @Operation(summary = "Получить информацию о себе")
+ @ApiResponse(responseCode = "200", description = "Информация о пользователе получена")
+ @GetMapping("/self")
+ public ResponseEntity> getSelf(Authentication authentication) {
+ return ResponseEntity.ok(clientService.getSelf(authentication));
+ }
+
+ @Operation(summary = "Получить все свои счета")
+ @ApiResponse(responseCode = "200", description = "Счета получены")
+ @GetMapping("/accounts")
+ public ResponseEntity> getMyAccounts(Authentication authentication) {
+ return ResponseEntity.ok(clientService.getMyAccounts(authentication));
+ }
+
+ @Operation(summary = "Получить информацию о счете по ID")
+ @ApiResponse(responseCode = "200", description = "Информация о счете получена")
+ @GetMapping("/accounts/{id}")
+ public ResponseEntity> getAccountById(@PathVariable long id, Authentication authentication) {
+ return ResponseEntity.ok(clientService.getAccountById(id, authentication));
+ }
+
+ @Operation(summary = "Добавить друга")
+ @ApiResponse(responseCode = "200", description = "Друг добавлен")
+ @PostMapping("/add_friend/{friendId}")
+ public ResponseEntity> addFriend(@PathVariable("friendId") long friendId, Authentication authentication) {
+ clientService.addFriend(friendId, authentication);
+ return ResponseEntity.ok().build();
+ }
+
+ @Operation(summary = "Удалить друга")
+ @ApiResponse(responseCode = "200", description = "Друг удален")
+ @DeleteMapping("/delete_friend/{friendId}")
+ public ResponseEntity> deleteFriend(@PathVariable("friendId") long friendId, Authentication authentication) {
+ clientService.deleteFriend(friendId, authentication);
+ return ResponseEntity.ok().build();
+ }
+
+ @Operation(summary = "Перевод средств")
+ @ApiResponse(responseCode = "200", description = "Перевод выполнен")
+ @PostMapping("/transfer")
+ public ResponseEntity> transfer(@RequestBody TransferRequest transferRequest, Authentication authentication) {
+ clientService.transfer(transferRequest, authentication);
+ return ResponseEntity.ok().build();
+ }
+
+ @Operation(summary = "Пополнить счет")
+ @ApiResponse(responseCode = "200", description = "Счет пополнен")
+ @PostMapping("/accounts/{id}/deposit")
+ public ResponseEntity> deposit(@PathVariable("id") long id, @RequestParam("amount") double amount,
+ Authentication authentication) {
+ clientService.deposit(id, amount, authentication);
+ return ResponseEntity.ok().build();
+ }
+
+ @Operation(summary = "Снять средства со счета")
+ @ApiResponse(responseCode = "200", description = "Средства сняты")
+ @PostMapping("/accounts/{id}/withdraw")
+ public ResponseEntity> withdraw(@PathVariable("id") long id, @RequestParam("amount") double amount,
+ Authentication authentication) {
+ clientService.withdraw(id, amount, authentication);
+ return ResponseEntity.ok().build();
+ }
+
+ @GetMapping("/friends-and-accounts")
+ public ResponseEntity> getFriendsAndAccounts(Authentication authentication) {
+ return ResponseEntity.ok(clientService.getFriendsAndAccounts(authentication));
+ }
+}
diff --git a/API-Gateway/src/main/resources/application.properties b/API-Gateway/src/main/resources/application.properties
new file mode 100644
index 0000000..f0ec5a4
--- /dev/null
+++ b/API-Gateway/src/main/resources/application.properties
@@ -0,0 +1,24 @@
+server.port=8082
+
+bank.api.url=http://localhost:8081
+bank.api.port=8081
+bank.api.host=localhost
+jwt.secret=a-string-secret-at-least-256-bits-long
+jwt.expiration=3600000
+
+spring.datasource.url=jdbc:postgresql://localhost:54321/bank
+spring.datasource.username=postgres
+spring.datasource.password=postgres
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+
+springdoc.api-docs.enabled=true
+springdoc.swagger-ui.enabled=true
+
+logging.level.org.springframework=DEBUG
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e935f9..380b2c7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,333 @@
-# Java-projects
+# Banking System
+
+Многомодульный backend-проект на Java и Spring Boot, реализующий банковую систему с REST API, отдельным API Gateway, JWT-аутентификацией и асинхронной обработкой событий через Kafka.
+
+Проект собран как набор связанных сервисов:
+
+- **bank-presentation** — основной HTTP API банковой системы
+- **bank-application** — бизнес-логика
+- **bank-infrastructure** — сущности, DTO, репозитории и общая инфраструктура
+- **API-Gateway** — точка входа с аутентификацией и ролевым доступом
+- **Storage** — сервис-потребитель Kafka-событий с сохранением истории изменений в БД
+
+## Что реализовано
+
+Система поддерживает:
+
+- регистрацию и хранение пользователей;
+- создание банковских счетов;
+- пополнение счёта;
+- снятие средств;
+- переводы между счетами;
+- просмотр баланса;
+- просмотр истории транзакций;
+- работу с друзьями пользователя;
+- фильтрацию пользователей по полу и цвету волос;
+- ролевую модель **ADMIN / CLIENT**;
+- вход по логину и паролю с получением **JWT**;
+- выход с занесением токена в blacklist;
+- публикацию событий об изменении пользователей и счетов в **Kafka**;
+- отдельное асинхронное хранилище событий.
+
+## Архитектура
+
+### 1. Основной банковский сервис
+
+Бизнес-логика разделена на три модуля:
+
+#### `bank-application`
+
+Содержит прикладные сервисы и мапперы:
+
+- `AccountService`
+- `UserService`
+- `KafkaEventProducer`
+- `AccountMapper`
+- `UserMapper`
+- `TransactionMapper`
+
+Основные сценарии:
+
+- создание счёта для пользователя;
+- депозиты и снятие средств;
+- переводы между счетами;
+- расчёт комиссии при переводах;
+- получение счетов и транзакций;
+- регистрация пользователей;
+- добавление и удаление друзей;
+- отправка событий в Kafka-топики `client-topic` и `account-topic`.
+
+#### `bank-infrastructure`
+
+Содержит инфраструктурный слой:
+
+- JPA-сущности `User`, `Account`, `Transaction`;
+- DTO для API и событий;
+- Spring Data JPA репозитории;
+- enums для статусов и типов транзакций;
+- собственные исключения и глобальный обработчик ошибок.
+
+#### `bank-presentation`
+
+Содержит REST-контроллеры:
+
+- `UserController`
+- `AccountController`
+- `BankApplication`
+
+Основные endpoint'ы покрывают:
+
+- пользователей;
+- друзей пользователя;
+- пользовательские счета;
+- создание счетов;
+- баланс;
+- переводы;
+- историю транзакций;
+- фильтрацию транзакций по типу и счёту.
+
+### 2. API Gateway
+
+`API-Gateway` — отдельное Spring Boot приложение, выступающее внешней точкой входа.
+
+Что делает gateway:
+
+- аутентифицирует пользователей;
+- генерирует JWT;
+- валидирует JWT в `JwtAuthFilter`;
+- ограничивает доступ по ролям;
+- хранит учётные записи gateway в таблице `gateway_users`;
+- хранит отозванные токены в `token_blacklist`;
+- проксирует вызовы в основной банковский API через `RestTemplate`.
+
+Основные компоненты:
+
+- `AuthController`
+- `AdminController`
+- `ClientController`
+- `AuthService`
+- `AdminServices`
+- `ClientServices`
+- `JwtServices`
+- `UserDetailsServiceImpl`
+- `SecurityConfig`
+
+Ролевой доступ:
+
+- **ADMIN** — создание администраторов и клиентов, просмотр пользователей и счетов;
+- **CLIENT** — просмотр своих данных, работа с друзьями, пополнение, снятие и переводы.
+
+### 3. Storage
+
+`Storage` — отдельный сервис для асинхронной фиксации событий.
+
+Он:
+
+- подключается к Kafka;
+- читает сообщения из топиков;
+- сохраняет события в PostgreSQL;
+- хранит отдельно события по клиентам и счетам.
+
+Основные компоненты:
+
+- `KafkaConsumer`
+- `AccountEvent`
+- `ClientEvent`
+- `AccountEventRepository`
+- `ClientEventRepository`
+
+## Особенности бизнес-логики
+
+### Переводы и комиссия
+
+В `AccountService` реализована комиссия при переводе:
+
+- **0%** — перевод самому себе;
+- **3%** — перевод другу;
+- **10%** — перевод другому пользователю, который не находится в списке друзей.
+
+### Транзакции
+
+Поддерживаются типы операций:
+
+- `DEPOSIT`
+- `WITHDRAW`
+- `TRANSFER`
+
+Поддерживаются статусы транзакций:
+
+- `PENDING`
+- `SUCCESS`
+- `FAILED`
+
+### Пользователи
+
+Пользователь содержит:
+
+- логин;
+- имя;
+- возраст;
+- пол;
+- цвет волос;
+- список друзей;
+- список счетов.
+
+## Технологический стек
+
+- **Java 23**
+- **Spring Boot 3.4.4**
+- **Spring Web**
+- **Spring Data JPA**
+- **Spring Security 6**
+- **PostgreSQL**
+- **Kafka**
+- **JWT (jjwt 0.12.6)**
+- **Swagger / Springdoc OpenAPI**
+- **MapStruct**
+- **Lombok**
+- **Maven**
+- **Docker Compose**
+- **GitHub Actions**
+
+## Структура репозитория
+
+```text
+.
+├── API-Gateway
+├── Storage
+├── bank-application
+├── bank-infrastructure
+├── bank-presentation
+├── docker-compose.yml
+└── pom.xml
+```
+
+Корневой `pom.xml` — агрегирующий parent-проект с модулями:
+
+- `bank-application`
+- `bank-infrastructure`
+- `bank-presentation`
+- `API-Gateway`
+- `Storage`
+
+## Инфраструктура и порты
+
+### PostgreSQL и pgAdmin
+
+В корневом `docker-compose.yml` поднимаются:
+
+- **PostgreSQL** на `localhost:54321`
+- **pgAdmin** на `localhost:8080`
+
+Параметры БД:
+
+- database: `bank`
+- user: `postgres`
+- password: `postgres`
+
+### Kafka
+
+В `Storage/docker-compose.yml` поднимаются:
+
+- **Zookeeper** на `2181`
+- **Kafka** на `9092`
+
+### Порты приложений
+
+- **bank-presentation** — `8081`
+- **API-Gateway** — `8082`
+- **Storage** — `8083`
+
+## Безопасность
+
+Безопасность вынесена в `API-Gateway`.
+
+Реализовано:
+
+- stateless-конфигурация через `SessionCreationPolicy.STATELESS`;
+- вход через `/api/auth/login`;
+- JWT-фильтр `JwtAuthFilter`;
+- шифрование паролей через `BCryptPasswordEncoder`;
+- blacklist токенов при logout;
+- разделение прав через `@PreAuthorize`.
+
+Основные защищённые зоны:
+
+- `/api/admin/**` — только `ADMIN`;
+- `/api/client/**` — только `CLIENT`.
+
+## Документация API
+
+Swagger/OpenAPI включён в:
+
+- `bank-presentation`
+- `API-Gateway`
+
+Судя по конфигурации, приложение публикует OpenAPI-документацию и Swagger UI через `springdoc`.
+
+## Событийная модель
+
+При изменениях в системе публикуются события двух типов:
+
+- **Client events** — создание и изменение пользователей;
+- **Account events** — создание счетов, изменение баланса и новые операции.
+
+Банковый сервис публикует события в Kafka, а `Storage` сохраняет их в БД. Это позволяет отделить основную бизнес-логику от слоя аудита и асинхронного хранения истории изменений.
+
+## Сборка и запуск
+
+### 1. Клонирование репозитория
+
+```bash
+git clone https://github.com/EpicWhal3/Java-projects.git
+cd Java-projects
+```
+
+### 2. Поднять PostgreSQL и pgAdmin
+
+```bash
+docker compose up -d
+```
+
+### 3. Поднять Kafka и Zookeeper
+
+```bash
+cd Storage
+docker compose up -d
+cd ..
+```
+
+### 4. Собрать проект
+
+```bash
+mvn clean install
+```
+
+### 5. Запустить приложения
+
+В отдельных терминалах:
+
+#### bank-presentation
+
+```bash
+cd bank-presentation
+mvn spring-boot:run
+```
+
+#### API-Gateway
+
+```bash
+cd API-Gateway
+mvn spring-boot:run
+```
+
+#### Storage
+
+```bash
+cd Storage
+mvn spring-boot:run
+```
+
+## CI
+
+В репозитории настроен GitHub Actions workflow `.github/workflows/java.yml`, который собирает проект через Maven при `push`.
diff --git a/Storage/docker-compose.yml b/Storage/docker-compose.yml
new file mode 100644
index 0000000..29f055d
--- /dev/null
+++ b/Storage/docker-compose.yml
@@ -0,0 +1,23 @@
+version: '3.8'
+services:
+ zookeeper:
+ image: confluentinc/cp-zookeeper:7.5.0
+ container_name: zookeeper
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+ ports:
+ - "2181:2181"
+
+ kafka:
+ image: confluentinc/cp-kafka:7.5.0
+ container_name: kafka
+ depends_on:
+ - zookeeper
+ ports:
+ - "9092:9092"
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
diff --git a/Storage/pom.xml b/Storage/pom.xml
new file mode 100644
index 0000000..f98b61e
--- /dev/null
+++ b/Storage/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+
+
+ Storage
+ 1.0-SNAPSHOT
+
+
+ 23
+ 23
+ UTF-8
+
+
+
+ org.bank
+ bank-infrastructure
+ 1.0-SNAPSHOT
+ compile
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.1
+
+
+
+
\ No newline at end of file
diff --git a/Storage/src/main/java/org/storage/StorageApplication.java b/Storage/src/main/java/org/storage/StorageApplication.java
new file mode 100644
index 0000000..baebd75
--- /dev/null
+++ b/Storage/src/main/java/org/storage/StorageApplication.java
@@ -0,0 +1,11 @@
+package org.storage;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(scanBasePackages = "org.storage")
+public class StorageApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(StorageApplication.class, args);
+ }
+}
diff --git a/Storage/src/main/java/org/storage/consumers/KafkaConsumer.java b/Storage/src/main/java/org/storage/consumers/KafkaConsumer.java
new file mode 100644
index 0000000..f4e4fa2
--- /dev/null
+++ b/Storage/src/main/java/org/storage/consumers/KafkaConsumer.java
@@ -0,0 +1,34 @@
+package org.storage.consumers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Service;
+import org.bank.memory.DTO_entities.AccountEventDto;
+import org.bank.memory.DTO_entities.ClientEventDto;
+import org.storage.entities.AccountEvent;
+import org.storage.entities.ClientEvent;
+import org.storage.repo.AccountEventRepository;
+import org.storage.repo.ClientEventRepository;
+
+@Service
+@RequiredArgsConstructor
+public class KafkaConsumer {
+
+ private final ObjectMapper objectMapper;
+ private final AccountEventRepository accountRepo;
+ private final ClientEventRepository clientRepo;
+
+ @KafkaListener(topics = "account-topic", groupId = "storage-group")
+ public void listenAccountTopic(String message) throws Exception {
+ AccountEventDto dto = objectMapper.readValue(message, AccountEventDto.class);
+ accountRepo.save(AccountEvent.fromDTO(dto));
+ }
+
+ @KafkaListener(topics = "client-topic", groupId = "storage-group")
+ public void listenClientTopic(String message) throws Exception {
+ ClientEventDto dto = objectMapper.readValue(message, ClientEventDto.class);
+ clientRepo.save(ClientEvent.fromDTO(dto));
+ }
+}
+
diff --git a/Storage/src/main/java/org/storage/entities/AccountEvent.java b/Storage/src/main/java/org/storage/entities/AccountEvent.java
new file mode 100644
index 0000000..4d02930
--- /dev/null
+++ b/Storage/src/main/java/org/storage/entities/AccountEvent.java
@@ -0,0 +1,59 @@
+package org.storage.entities;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bank.memory.DTO_entities.AccountEventDto;
+import org.springframework.lang.NonNull;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "account_events")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AccountEvent {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String eventType;
+ private LocalDateTime eventTime;
+
+ private Long accountId;
+ private Double balance;
+ private String ownerLogin;
+
+ private String changedField;
+ private String oldValue;
+ private String newValue;
+
+ private Long transactionId;
+ private String transactionType;
+ private Double transactionAmount;
+
+ public static @NonNull AccountEvent fromDTO(@NonNull AccountEventDto dto) {
+ AccountEvent entity = new AccountEvent();
+ entity.setEventType(dto.getEventType());
+ entity.setEventTime(dto.getEventTime());
+ entity.setAccountId(dto.getAccountId());
+ entity.setOwnerLogin(dto.getOwnerLogin());
+ entity.setBalance(dto.getBalance());
+
+ if (dto.getChanges() != null) {
+ entity.setChangedField(dto.getChanges().getChangedField());
+ entity.setOldValue(dto.getChanges().getOldValue().toString());
+ entity.setNewValue(dto.getChanges().getNewValue().toString());
+ }
+
+ if (dto.getLastTransaction() != null) {
+ entity.setTransactionId(dto.getLastTransaction().getId());
+ entity.setTransactionType(dto.getLastTransaction().getType().toString());
+ entity.setTransactionAmount(dto.getLastTransaction().getAmount());
+ }
+
+ return entity;
+ }
+}
diff --git a/Storage/src/main/java/org/storage/entities/ClientEvent.java b/Storage/src/main/java/org/storage/entities/ClientEvent.java
new file mode 100644
index 0000000..09706ed
--- /dev/null
+++ b/Storage/src/main/java/org/storage/entities/ClientEvent.java
@@ -0,0 +1,50 @@
+package org.storage.entities;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bank.memory.DTO_entities.ClientEventDto;
+import org.springframework.lang.NonNull;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "client_events")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClientEvent {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String eventType;
+ private LocalDateTime eventTime;
+
+ private Long userId;
+ private String name;
+ private Integer age;
+ private String gender;
+ private String hairColor;
+
+ private String accountIds;
+ private String friendLogins;
+ private String changedField;
+ private String oldValue;
+ private String newValue;
+
+
+ public static @NonNull ClientEvent fromDTO(@NonNull ClientEventDto dto) {
+ ClientEvent entity = new ClientEvent();
+ entity.setEventType(dto.getEventType());
+ entity.setEventTime(dto.getEventTime());
+ entity.setUserId(dto.getUserId());
+ entity.setName(dto.getName());
+ entity.setAge(dto.getAge());
+ entity.setGender(dto.getGender());
+ entity.setHairColor(dto.getHairColor() != null ? dto.getHairColor().toString() : null);
+ return entity;
+ }
+}
+
diff --git a/Storage/src/main/java/org/storage/repo/AccountEventRepository.java b/Storage/src/main/java/org/storage/repo/AccountEventRepository.java
new file mode 100644
index 0000000..49ab35e
--- /dev/null
+++ b/Storage/src/main/java/org/storage/repo/AccountEventRepository.java
@@ -0,0 +1,8 @@
+package org.storage.repo;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.storage.entities.AccountEvent;
+
+public interface AccountEventRepository extends JpaRepository {
+
+}
diff --git a/Storage/src/main/java/org/storage/repo/ClientEventRepository.java b/Storage/src/main/java/org/storage/repo/ClientEventRepository.java
new file mode 100644
index 0000000..2d4966c
--- /dev/null
+++ b/Storage/src/main/java/org/storage/repo/ClientEventRepository.java
@@ -0,0 +1,8 @@
+package org.storage.repo;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.storage.entities.ClientEvent;
+
+public interface ClientEventRepository extends JpaRepository {
+
+}
diff --git a/Storage/src/main/resources/application.properties b/Storage/src/main/resources/application.properties
new file mode 100644
index 0000000..e7be4e9
--- /dev/null
+++ b/Storage/src/main/resources/application.properties
@@ -0,0 +1,25 @@
+server.port=8083
+
+spring.kafka.bootstrap-servers=localhost:9092
+spring.kafka.consumer.group-id=storage-group
+spring.kafka.consumer.auto-offset-reset=earliest
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+spring.kafka.listener.missing-topics-fatal=false
+spring.kafka.listener.ack-mode=MANUAL_IMMEDIATE
+
+spring.datasource.url=jdbc:postgresql://localhost:54321/bank
+spring.datasource.username=postgres
+spring.datasource.password=postgres
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+
+
+logging.level.org.springframework=DEBUG
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
\ No newline at end of file
diff --git a/bank-application/pom.xml b/bank-application/pom.xml
new file mode 100644
index 0000000..0cef0f7
--- /dev/null
+++ b/bank-application/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+
+
+ bank-application
+
+
+ 23
+ 23
+ UTF-8
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ test
+
+
+
+ org.bank
+ bank-infrastructure
+ 1.0-SNAPSHOT
+ compile
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.1
+
+
+
+
+
\ No newline at end of file
diff --git a/bank-application/src/main/java/org/bank/core/mappers/AccountMapper.java b/bank-application/src/main/java/org/bank/core/mappers/AccountMapper.java
new file mode 100644
index 0000000..ef4144e
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/mappers/AccountMapper.java
@@ -0,0 +1,15 @@
+package org.bank.core.mappers;
+
+import org.bank.memory.DTO_entities.AccountDTO;
+import org.bank.memory.entities.accounts.Account;
+import org.mapstruct.Mapper;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@Mapper(componentModel = "spring")
+public interface AccountMapper {
+ AccountDTO toAccountDTO(Account account);
+ List toAccountDTOs(List accounts);
+}
diff --git a/bank-application/src/main/java/org/bank/core/mappers/TransactionMapper.java b/bank-application/src/main/java/org/bank/core/mappers/TransactionMapper.java
new file mode 100644
index 0000000..da0af60
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/mappers/TransactionMapper.java
@@ -0,0 +1,14 @@
+package org.bank.core.mappers;
+
+
+import org.bank.memory.DTO_entities.TransactionDTO;
+import org.bank.memory.entities.transactions.Transaction;
+import org.mapstruct.Mapper;
+
+import java.util.List;
+
+@Mapper(componentModel = "spring")
+public interface TransactionMapper {
+ TransactionDTO toTransactionDTO(Transaction transaction);
+ List toTransactionDTOs(List transactions);
+}
diff --git a/bank-application/src/main/java/org/bank/core/mappers/UserMapper.java b/bank-application/src/main/java/org/bank/core/mappers/UserMapper.java
new file mode 100644
index 0000000..59e3935
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/mappers/UserMapper.java
@@ -0,0 +1,27 @@
+package org.bank.core.mappers;
+
+import org.bank.memory.DTO_entities.UserDTO;
+import org.bank.memory.entities.users.User;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Mapper(componentModel = "spring")
+public interface UserMapper {
+
+ @Mapping(target = "friends", source = "friends", qualifiedByName = "usersToIds")
+ UserDTO toUserDTO(User user);
+
+ List toUserDTOs(List users);
+
+ @Named("usersToIds")
+ default List toIds(List users) {
+ if (users.isEmpty()) {
+ return List.of();
+ }
+ return users.stream().map(User::getId).collect(Collectors.toList());
+ }
+}
\ No newline at end of file
diff --git a/bank-application/src/main/java/org/bank/core/services/AccountService.java b/bank-application/src/main/java/org/bank/core/services/AccountService.java
new file mode 100644
index 0000000..1a22411
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/services/AccountService.java
@@ -0,0 +1,295 @@
+package org.bank.core.services;
+
+import lombok.RequiredArgsConstructor;
+import org.bank.memory.DTO_entities.AccountEventDto;
+import org.bank.memory.DTO_entities.AccountDTO;
+import org.bank.memory.DTO_entities.TransactionDTO;
+import org.bank.memory.entities.transactions.Transaction;
+import org.bank.memory.entities.transactions.TransactionStatus;
+import org.bank.memory.entities.transactions.TransactionTypes;
+import org.bank.memory.entities.accounts.Account;
+import org.bank.memory.entities.users.User;
+import org.bank.memory.exceptions.AccountExceptions;
+import org.bank.memory.exceptions.UserExceptions;
+import org.bank.core.mappers.AccountMapper;
+import org.bank.core.mappers.TransactionMapper;
+import org.bank.memory.repos.AccountRepository;
+import org.bank.memory.repos.TransactionRepository;
+import org.bank.memory.repos.UserRepository;
+import org.bank.memory.requestEntites.TransferRequestBody;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Класс для работы со счётами пользователей.
+ */
+@Service
+@RequiredArgsConstructor
+public class AccountService {
+
+ private final AccountRepository accountRepository;
+ private final TransactionRepository transactionRepository;
+ private final UserRepository userRepository;
+ private final AccountMapper accountMapper;
+ private final TransactionMapper transactionMapper;
+ private final KafkaEventProducer kafkaProducer;
+
+ /**
+ * Метод для создания счёта.
+ *
+ * @param login логин пользователя
+ */
+ @Transactional
+ public AccountDTO createAccount(String login) throws Exception {
+ User user = userRepository.findByLogin(login);
+ if (user == null) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден");
+ }
+ Account account = new Account(user);
+ accountRepository.save(account);
+
+ AccountEventDto event = AccountEventDto.fromAccount(account, "CREATED");
+ kafkaProducer.sendAccountEvent(event);
+ return accountMapper.toAccountDTO(account);
+ }
+
+ /**
+ * Метод для пополнения счёта.
+ *
+ * @param accountId идентификатор счёта
+ * @param amount сумма для пополнения
+ * @throws AccountExceptions исключение, если счёт не найден
+ */
+ @Transactional
+ public AccountDTO deposit(@NonNull Long accountId, double amount) throws Exception {
+ Optional account = accountRepository.findById(accountId);
+ if (account.isEmpty()) {
+ throw AccountExceptions.AccountNotFoundException("Счет не найден");
+ }
+ double oldBalance = account.get().getBalance();
+ account.get().deposit(amount);
+ Transaction tx = new Transaction(TransactionTypes.DEPOSIT,
+ TransactionStatus.SUCCESS, amount, "Пополнение счета", account.get());
+ account.get().addTransaction(tx);
+ transactionRepository.save(tx);
+
+ AccountEventDto event = AccountEventDto.fromAccount(account.get(), "UPDATED");
+ event.setChanges(new AccountEventDto.FieldChanges(
+ "balance",
+ oldBalance,
+ account.get().getBalance()));
+ event.setLastTransaction(new AccountEventDto.TransactionSummary(
+ tx.getId(),
+ tx.getType(),
+ tx.getAmount()));
+ kafkaProducer.sendAccountEvent(event);
+ return accountMapper.toAccountDTO(account.get());
+ }
+
+ /**
+ * Метод для снятия средств со счёта.
+ *
+ * @param accountId идентификатор счёта
+ * @param amount сумма для снятия
+ * @throws AccountExceptions исключение, если счёт не найден
+ */
+ @Transactional
+ public AccountDTO withdraw(@NonNull Long accountId, double amount) throws Exception {
+ Account account = accountRepository.findById(accountId).orElse(null);
+ if (account == null) {
+ throw AccountExceptions.AccountNotFoundException("Счет не найден");
+ }
+ double oldBalance = account.getBalance();
+ account.withdraw(amount);
+ Transaction tx = new Transaction(TransactionTypes.WITHDRAW,
+ TransactionStatus.SUCCESS, amount, "Снятие со счета", account);
+ account.addTransaction(tx);
+ transactionRepository.save(tx);
+
+ AccountEventDto event = AccountEventDto.fromAccount(account, "UPDATED");
+ event.setChanges(new AccountEventDto.FieldChanges(
+ "balance",
+ oldBalance,
+ account.getBalance()));
+ event.setLastTransaction(new AccountEventDto.TransactionSummary(
+ tx.getId(),
+ tx.getType(),
+ tx.getAmount()));
+ kafkaProducer.sendAccountEvent(event);
+
+ return accountMapper.toAccountDTO(account);
+ }
+
+ /**
+ * Метод для перевода средств между счетами.
+ *
+ * @param transferRequest тело запроса на перевод средств
+ * @throws AccountExceptions исключение, если счёт не найден
+ */
+ @Transactional
+ public ArrayList transfer(TransferRequestBody transferRequest)
+ throws Exception {
+ Long fromId = transferRequest.getFromAccountId();
+ Long toId = transferRequest.getToAccountId();
+ double amount = transferRequest.getAmount();
+
+ Account fromAccount = accountRepository.findById(fromId).orElse(null);
+ Account toAccount = accountRepository.findById(toId).orElse(null);
+
+ if (fromAccount == null || toAccount == null) {
+ throw AccountExceptions.AccountNotFoundException("Счет не найден");
+ }
+
+ User sender = userRepository.findByLogin(fromAccount.getOwner().getLogin());
+ User recipient = userRepository.findByLogin(toAccount.getOwner().getLogin());
+ if (sender == null || recipient == null) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден");
+ }
+
+ double commissionRate = CommissionCalc(sender, recipient);
+ double totalAmount = amount + (amount * commissionRate);
+
+ double fromOldBalance = fromAccount.getBalance();
+ double toOldBalance = toAccount.getBalance();
+
+ fromAccount.withdraw(totalAmount);
+ toAccount.deposit(amount);
+
+ Transaction txFrom = new Transaction(TransactionTypes.TRANSFER,
+ TransactionStatus.SUCCESS, totalAmount, "Перевод средств на счёт " + toAccount.getId(),
+ fromAccount);
+ Transaction txTo = new Transaction(TransactionTypes.TRANSFER,
+ TransactionStatus.SUCCESS, amount, "Получение средств со счёта " + fromAccount.getId(),
+ toAccount);
+
+ transactionRepository.save(txFrom);
+ transactionRepository.save(txTo);
+ AccountEventDto fromEvent = AccountEventDto.fromAccount(fromAccount, "UPDATED");
+ fromEvent.setChanges(new AccountEventDto.FieldChanges(
+ "balance",
+ fromOldBalance,
+ fromAccount.getBalance()));
+ fromEvent.setLastTransaction(new AccountEventDto.TransactionSummary(
+ txFrom.getId(),
+ txFrom.getType(),
+ txFrom.getAmount()));
+
+ AccountEventDto toEvent = AccountEventDto.fromAccount(toAccount, "UPDATED");
+ toEvent.setChanges(new AccountEventDto.FieldChanges(
+ "balance",
+ toOldBalance,
+ toAccount.getBalance()));
+ toEvent.setLastTransaction(new AccountEventDto.TransactionSummary(
+ txTo.getId(),
+ txTo.getType(),
+ txTo.getAmount()));
+
+ kafkaProducer.sendAccountEvent(fromEvent);
+ kafkaProducer.sendAccountEvent(toEvent);
+
+ return new ArrayList<>(List.of(accountMapper.toAccountDTO(fromAccount),
+ accountMapper.toAccountDTO(toAccount)));
+ }
+
+ /**
+ * Метод для проверки баланса счёта.
+ *
+ * @param id идентификатор счёта
+ */
+ @Transactional(readOnly = true)
+ public AccountDTO checkBalance(@NonNull Long id) throws AccountExceptions {
+ Optional account = accountRepository.findById(id);
+ if (account.isEmpty()) {
+ throw AccountExceptions.AccountNotFoundException("Счет не найден");
+ }
+ return new AccountDTO(account.get().getId(), account.get().getBalance());
+ }
+
+ private double CommissionCalc(User sender, User recipient) {
+ double commissionRate = 0.0;
+ if (!sender.getLogin().equals(recipient.getLogin())) {
+ commissionRate = sender.getFriends().contains(recipient) ? 0.03 : 0.10;
+ }
+ return commissionRate;
+ }
+
+ /**
+ * Метод для получения всех счетов.
+ *
+ * @return список счетов пользователя
+ */
+ @Transactional(readOnly = true)
+ public List getAllAccounts() throws AccountExceptions {
+ List accounts = accountRepository.findAll();
+ if (accounts.isEmpty()) {
+ throw AccountExceptions.NoAccounts("Счета не найдены");
+ }
+ return accountMapper.toAccountDTOs(accounts);
+ }
+
+ /**
+ * Метод для получения всех операций.
+ *
+ * @return список операций
+ */
+ @Transactional(readOnly = true)
+ public List getAllTransactions() throws AccountExceptions {
+ List transactions = transactionRepository.findAll();
+ if (transactions.isEmpty()) {
+ throw AccountExceptions.NoTransactionsException("Нет операций по счету");
+ }
+ return transactionMapper.toTransactionDTOs(transactions);
+ }
+
+ /**
+ * Метод для получения всех операций по идентификатору счета.
+ *
+ * @param id идентификатор счета
+ * @return список операций
+ */
+ @Transactional(readOnly = true)
+ public List getAllTransactionsById(Long id) throws AccountExceptions {
+ List transactions = transactionRepository.findByAccountId(id);
+ if (transactions.isEmpty()) {
+ throw AccountExceptions.NoTransactionsException("Нет операций по счету");
+ }
+ return transactionMapper.toTransactionDTOs(transactions);
+ }
+
+ /**
+ * Метод для получения всех операций по типу операции.
+ *
+ * @param transactionTypes тип операции
+ * @return список операций
+ */
+ @Transactional(readOnly = true)
+ public List getAllTransactionsByType(TransactionTypes transactionTypes) throws AccountExceptions {
+ List transactions = transactionRepository.findTransactionByType(transactionTypes);
+ if (transactions.isEmpty()) {
+ throw AccountExceptions.NoTransactionsException("Нет операций по счету");
+ }
+ return transactionMapper.toTransactionDTOs(transactions);
+ }
+
+ /**
+ * Метод для получения всех операций по идентификатору счета и типу операции.
+ *
+ * @param id идентификатор счета
+ * @param transactionTypes тип операции
+ * @return список операций
+ */
+ @Transactional(readOnly = true)
+ public List getAllTransactionsByIdAndType(Long id, TransactionTypes transactionTypes)
+ throws AccountExceptions {
+ List transactions = transactionRepository.findByAccountIdAndType(id, transactionTypes);
+ if (transactions.isEmpty()) {
+ throw AccountExceptions.NoTransactionsException("Нет операций по счету");
+ }
+ return transactionMapper.toTransactionDTOs(transactions);
+ }
+}
diff --git a/bank-application/src/main/java/org/bank/core/services/KafkaEventProducer.java b/bank-application/src/main/java/org/bank/core/services/KafkaEventProducer.java
new file mode 100644
index 0000000..719c3fb
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/services/KafkaEventProducer.java
@@ -0,0 +1,26 @@
+package org.bank.core.services;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.bank.memory.DTO_entities.AccountEventDto;
+import org.bank.memory.DTO_entities.ClientEventDto;
+
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class KafkaEventProducer {
+ private final KafkaTemplate kafkaTemplate;
+ private final ObjectMapper objectMapper;
+
+ public void sendClientEvent(ClientEventDto event) throws Exception {
+ String message = objectMapper.writeValueAsString(event);
+ kafkaTemplate.send("client-topic", event.getUserId().toString(), message);
+ }
+
+ public void sendAccountEvent(AccountEventDto event) throws Exception {
+ String message = objectMapper.writeValueAsString(event);
+ kafkaTemplate.send("account-topic", event.getAccountId().toString(), message);
+ }
+}
diff --git a/bank-application/src/main/java/org/bank/core/services/UserService.java b/bank-application/src/main/java/org/bank/core/services/UserService.java
new file mode 100644
index 0000000..4927689
--- /dev/null
+++ b/bank-application/src/main/java/org/bank/core/services/UserService.java
@@ -0,0 +1,227 @@
+package org.bank.core.services;
+
+import lombok.RequiredArgsConstructor;
+import org.bank.memory.DTO_entities.ClientEventDto;
+import org.bank.memory.DTO_entities.AccountDTO;
+import org.bank.memory.DTO_entities.UserDTO;
+import org.bank.memory.entities.users.HairColor;
+import org.bank.memory.entities.users.User;
+import org.bank.memory.exceptions.UserExceptions;
+import org.bank.core.mappers.AccountMapper;
+import org.bank.core.mappers.UserMapper;
+import org.bank.memory.repos.UserRepository;
+import org.bank.memory.requestEntites.CreateUserRequest;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Класс для работы с пользователями.
+ */
+@Service
+@RequiredArgsConstructor
+public class UserService {
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+ private final AccountMapper accountMapper;
+ private final KafkaEventProducer kafkaProducer;
+
+ /**
+ * Метод для регистрации пользователя.
+ *
+ * @param createUserRequest данные пользователя
+ * @throws UserExceptions исключение, если пользователь уже существует
+ */
+ @Transactional
+ public UserDTO registerUser(CreateUserRequest createUserRequest) throws Exception {
+ HairColor hairColorEnum = HairColor.valueOf(createUserRequest.getHairColor().toUpperCase());
+ User user = new User(createUserRequest.getLogin(), createUserRequest.getName(),
+ createUserRequest.getAge(), createUserRequest.getGender(), hairColorEnum);
+ if (userRepository.existsByLogin(user.getLogin())) {
+ throw UserExceptions.UserAlreadyExistsException("Пользователь с таким логином уже существует.");
+ }
+
+ ClientEventDto event = ClientEventDto.fromUser(user, "CREATED");
+ kafkaProducer.sendClientEvent(event);
+ userRepository.save(user);
+ return userMapper.toUserDTO(user);
+ }
+
+ /**
+ * Метод для получения информации о пользователе.
+ *
+ * @param login логин пользователя
+ */
+ @Transactional(readOnly = true)
+ public UserDTO getUserInfo(String login) throws UserExceptions {
+ UserDTO userDTO = userMapper.toUserDTO(userRepository.findByLogin(login));
+ if (userDTO == null) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден.");
+ }
+ return userDTO;
+ }
+
+ /**
+ * Метод для добавления друга.
+ *
+ * @param user_id id пользователя
+ * @param friend_id id друга
+ */
+ @Transactional
+ public ArrayList addFriend(@NonNull Long user_id, @NonNull Long friend_id) throws Exception {
+ User user = userRepository.findById(user_id).orElse(null);
+ User friend = userRepository.findById(friend_id).orElse(null);
+
+ if (user == null || friend == null) {
+ throw UserExceptions.UserNotFoundException("Пользователь или друг не найден.");
+ }
+ if (user.getFriends().contains(friend)) {
+ throw UserExceptions.UserAlreadyExistsException("Друг уже добавлен.");
+ }
+ user.addFriend(friend);
+ friend.addFriend(user);
+
+ ClientEventDto userEvent = ClientEventDto.fromUser(user, "UPDATED");
+ userEvent.setChanges(new ClientEventDto.FieldChanges(
+ "friends",
+ user.getFriends().size() - 1,
+ user.getFriends().size()));
+
+ ClientEventDto friendEvent = ClientEventDto.fromUser(friend, "UPDATED");
+ friendEvent.setChanges(new ClientEventDto.FieldChanges(
+ "friends",
+ friend.getFriends().size() - 1,
+ friend.getFriends().size()));
+
+ kafkaProducer.sendClientEvent(userEvent);
+ kafkaProducer.sendClientEvent(friendEvent);
+
+ return new ArrayList<>(List.of(userMapper.toUserDTO(user), userMapper.toUserDTO(friend)));
+ }
+
+ /**
+ * Метод для удаления друга.
+ *
+ * @param user_id id пользователя
+ * @param friend_id id друга
+ */
+ @Transactional
+ public ArrayList deleteFriend(@NonNull Long user_id, @NonNull Long friend_id) throws UserExceptions {
+ User user = userRepository.findById(user_id).orElse(null);
+ User friend = userRepository.findById(friend_id).orElse(null);
+ if (user == null || friend == null) {
+ throw UserExceptions.UserNotFoundException("Пользователь или друг не найден.");
+ }
+ if (!user.getFriends().contains(friend)) {
+ throw UserExceptions.UserNotFoundException("Друг не найден.");
+ }
+ user.removeFriend(friend);
+ friend.removeFriend(user);
+
+ ClientEventDto userEvent = ClientEventDto.fromUser(user, "UPDATED");
+ userEvent.setChanges(new ClientEventDto.FieldChanges(
+ "friends",
+ user.getFriends().size() + 1,
+ user.getFriends().size()));
+
+ ClientEventDto friendEvent = ClientEventDto.fromUser(friend, "UPDATED");
+ friendEvent.setChanges(new ClientEventDto.FieldChanges(
+ "friends",
+ user.getFriends().size() + 1,
+ friend.getFriends().size()));
+
+ return new ArrayList<>(List.of(userMapper.toUserDTO(user), userMapper.toUserDTO(friend)));
+ }
+
+ /**
+ * Метод для получения счета пользователя по логину.
+ *
+ * @param userid логин пользователя
+ * @return счет пользователя
+ */
+ @Transactional(readOnly = true)
+ public List getUserAccounts(@NonNull Long userid) throws UserExceptions {
+ Optional user = userRepository.findById(userid);
+ if (user.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден.");
+ }
+ return accountMapper.toAccountDTOs(user.get().getAccounts());
+ }
+
+ /**
+ * Метод для получения всех друзей пользователя.
+ *
+ * @param userid логин пользователя
+ * @return список друзей пользователя
+ */
+ @Transactional(readOnly = true)
+ public List getUserFriends(@NonNull Long userid) throws UserExceptions {
+ Optional user = userRepository.findById(userid);
+ if (user.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден.");
+ }
+ UserDTO userDTO = userMapper.toUserDTO(user.get());
+ return userDTO.getFriends();
+ }
+
+ /**
+ * Метод для получения всех пользователей.
+ *
+ * @return список пользователей
+ */
+ @Transactional(readOnly = true)
+ public List getAll() throws UserExceptions {
+ List user = userRepository.findAll();
+ if (user.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователь не найден.");
+ }
+ return userMapper.toUserDTOs(user);
+ }
+
+ /**
+ * Метод для получения всех пользователей с фильтрацией по цвету волос и полу.
+ *
+ * @param hairColor цвет волос
+ * @param gender пол
+ * @return список пользователей
+ */
+ @Transactional(readOnly = true)
+ public List getFilteredUsers(HairColor hairColor, String gender) throws UserExceptions {
+ List users = userRepository.findByHairColorAndGender(hairColor, gender);
+ if (users.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователи не найдены.");
+ }
+ return userMapper.toUserDTOs(users);
+ }
+
+ /**
+ * Метод для получения всех пользователей с фильтрацией по цвету волос.
+ *
+ * @param hairColor цвет волос
+ * @return список пользователей
+ */
+ @Transactional(readOnly = true)
+ public List getFilteredUsersByHairColor(HairColor hairColor) throws UserExceptions {
+ List users = userRepository.findByHairColor(hairColor);
+ if (users.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователи не найдены.");
+ }
+ return userMapper.toUserDTOs(users);
+ }
+
+ /**
+ * Метод для получения всех пользователей с фильтрацией по полу.
+ */
+ @Transactional(readOnly = true)
+ public List getFilteredUsersByGender(String gender) throws UserExceptions {
+ List users = userRepository.findByGender(gender);
+ if (users.isEmpty()) {
+ throw UserExceptions.UserNotFoundException("Пользователи не найдены.");
+ }
+ return userMapper.toUserDTOs(users);
+ }
+}
\ No newline at end of file
diff --git a/bank-infrastructure/pom.xml b/bank-infrastructure/pom.xml
new file mode 100644
index 0000000..25e090c
--- /dev/null
+++ b/bank-infrastructure/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+
+
+ bank-infrastructure
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.1
+
+
+
+
+ 23
+ 23
+ UTF-8
+
+
\ No newline at end of file
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountDTO.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountDTO.java
new file mode 100644
index 0000000..1720f49
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountDTO.java
@@ -0,0 +1,20 @@
+package org.bank.memory.DTO_entities;
+
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Класс для создания DTO (Data Transfer Object) для аккаунта.
+ */
+@Getter
+@Setter
+public class AccountDTO {
+ private Long id;
+ private double balance;
+
+ public AccountDTO(Long id, double balance) {
+ this.id = id;
+ this.balance = balance;
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountEventDto.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountEventDto.java
new file mode 100644
index 0000000..d67cde0
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/AccountEventDto.java
@@ -0,0 +1,65 @@
+package org.bank.memory.DTO_entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.bank.memory.entities.accounts.Account;
+import org.bank.memory.entities.transactions.Transaction;
+import org.bank.memory.entities.transactions.TransactionTypes;
+
+import java.time.LocalDateTime;
+
+@Data
+public class AccountEventDto {
+ private String eventType;
+ private LocalDateTime eventTime = LocalDateTime.now();
+
+ private Long accountId;
+ private Double balance;
+ private String ownerLogin;
+
+ private FieldChanges changes;
+
+ private TransactionSummary lastTransaction;
+
+ public static AccountEventDto fromAccount(Account account, String eventType) {
+ AccountEventDto dto = new AccountEventDto();
+ dto.setEventType(eventType);
+ dto.setAccountId(account.getId());
+ dto.setBalance(account.getBalance());
+ dto.setOwnerLogin(account.getOwner().getLogin());
+
+ if (!account.getHistory().isEmpty()) {
+ Transaction lastTx = account.getHistory().get(account.getHistory().size() - 1);
+ dto.setLastTransaction(
+ new TransactionSummary(
+ lastTx.getId(),
+ lastTx.getType(),
+ lastTx.getAmount()
+ )
+ );
+ }
+
+ return dto;
+ }
+
+ @Data
+ @AllArgsConstructor
+ public static class FieldChanges {
+ private String changedField;
+ private Object oldValue;
+ private Object newValue;
+ }
+
+ @Data
+ public static class TransactionSummary {
+ private Long id;
+ private TransactionTypes type;
+ private Double amount;
+
+ public TransactionSummary(Long id, TransactionTypes type, Double amount) {
+ this.id = id;
+ this.type = type;
+ this.amount = amount;
+ }
+ }
+}
\ No newline at end of file
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ClientEventDto.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ClientEventDto.java
new file mode 100644
index 0000000..3e4a191
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ClientEventDto.java
@@ -0,0 +1,63 @@
+package org.bank.memory.DTO_entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.bank.memory.entities.accounts.Account;
+import org.bank.memory.entities.users.HairColor;
+import org.bank.memory.entities.users.User;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class ClientEventDto {
+ private String eventType;
+ private LocalDateTime eventTime = LocalDateTime.now();
+
+ private Long userId;
+ private String name;
+ private Integer age;
+ private String gender;
+ private HairColor hairColor;
+
+ private FieldChanges changes;
+ private List accountIds;
+ private List friendLogins;
+
+ public static ClientEventDto fromUser(User user, String eventType) {
+ ClientEventDto dto = new ClientEventDto();
+ dto.setEventType(eventType);
+ dto.setUserId(user.getId());
+ dto.setName(user.getName());
+ dto.setAge(user.getAge());
+ dto.setGender(user.getGender());
+ dto.setHairColor(user.getHairColor());
+
+ if (user.getAccounts() != null) {
+ dto.setAccountIds(
+ user.getAccounts().stream()
+ .map(Account::getId)
+ .collect(Collectors.toList())
+ );
+ }
+
+ if (user.getFriends() != null) {
+ dto.setFriendLogins(
+ user.getFriends().stream()
+ .map(User::getLogin)
+ .collect(Collectors.toList())
+ );
+ }
+
+ return dto;
+ }
+
+ @Data
+ @AllArgsConstructor
+ public static class FieldChanges {
+ private String changedField;
+ private Object oldValue;
+ private Object newValue;
+ }
+}
\ No newline at end of file
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ErrorResponse.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ErrorResponse.java
new file mode 100644
index 0000000..7b16d38
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/ErrorResponse.java
@@ -0,0 +1,16 @@
+package org.bank.memory.DTO_entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@AllArgsConstructor
+public class ErrorResponse {
+ private LocalDateTime timestamp;
+ private int status;
+ private String error;
+ private String message;
+ private String path;
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/TransactionDTO.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/TransactionDTO.java
new file mode 100644
index 0000000..67355f0
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/TransactionDTO.java
@@ -0,0 +1,27 @@
+package org.bank.memory.DTO_entities;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.bank.memory.entities.transactions.TransactionStatus;
+import org.bank.memory.entities.transactions.TransactionTypes;
+
+/**
+ * Класс для создания DTO (Data Transfer Object) для транзакции.
+ */
+@Getter
+@Setter
+public class TransactionDTO {
+ private Long id;
+ private TransactionTypes type;
+ private TransactionStatus status;
+ private String description;
+ private double amount;
+
+ public TransactionDTO(Long id, TransactionTypes type, TransactionStatus status, String description, double amount) {
+ this.id = id;
+ this.type = type;
+ this.status = status;
+ this.description = description;
+ this.amount = amount;
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/UserDTO.java b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/UserDTO.java
new file mode 100644
index 0000000..11aa2c4
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/DTO_entities/UserDTO.java
@@ -0,0 +1,20 @@
+package org.bank.memory.DTO_entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.bank.memory.entities.users.HairColor;
+
+import java.util.List;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class UserDTO {
+ private Long id;
+ private String name;
+ private int age;
+ private String gender;
+ private HairColor hairColor;
+ private List friends;
+}
\ No newline at end of file
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/accounts/Account.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/accounts/Account.java
new file mode 100644
index 0000000..ee2c16c
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/accounts/Account.java
@@ -0,0 +1,88 @@
+package org.bank.memory.entities.accounts;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+import org.bank.memory.entities.transactions.Transaction;
+import org.bank.memory.entities.transactions.TransactionStatus;
+import org.bank.memory.entities.users.User;
+import org.bank.memory.exceptions.AccountExceptions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Класс для создания счета пользователя.
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "accounts")
+public class Account {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "balance", nullable = false)
+ private Double balance;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "owner_login", referencedColumnName = "login")
+ private User owner;
+
+ @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List history;
+
+
+ /**
+ * Конструктор для создания счета пользователя.
+ *
+ * @param owner логин пользователя
+ */
+ public Account(User owner) {
+ this.owner = owner;
+ this.balance = 0.0;
+ this.history = new ArrayList<>();
+ }
+
+ public Account() {
+ }
+
+ /**
+ * Метод для пополнения счета.
+ *
+ * @param amount сумма пополнения
+ * @throws AccountExceptions исключение, если сумма депозита отрицательная
+ */
+ public void deposit(double amount) throws AccountExceptions {
+ if (amount <= 0) {
+ throw AccountExceptions.AmountIsNegativeException("Сумма депозита должна быть положительной.");
+ }
+
+ balance += amount;
+ }
+
+ /**
+ * Метод для снятия средств со счета.
+ *
+ * @param amount сумма снятия
+ * @throws AccountExceptions исключение, если сумма снятия отрицательная или недостаточно средств
+ */
+ public void withdraw(double amount) throws AccountExceptions {
+ if (amount <= 0) {
+ throw AccountExceptions.AmountIsNegativeException("Сумма должна быть положительной.");
+ }
+
+ if (amount > balance) {
+ throw AccountExceptions.InsufficientFundsException("Недостаточно средств: доступно " + balance);
+ }
+
+ balance -= amount;
+ }
+
+ public void addTransaction(Transaction tx) {
+ history.add(tx);
+ tx.setStatus(TransactionStatus.SUCCESS);
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/Transaction.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/Transaction.java
new file mode 100644
index 0000000..186faea
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/Transaction.java
@@ -0,0 +1,51 @@
+package org.bank.memory.entities.transactions;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+import org.bank.memory.entities.accounts.Account;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "transactions")
+public class Transaction {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private TransactionTypes type;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private TransactionStatus status;
+
+ @Column(nullable = false)
+ private Double amount;
+
+ private String description;
+
+ @Column(name = "timestamp", nullable = false)
+ private LocalDateTime timestamp;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "account_id", nullable = false)
+ private Account account;
+
+ public Transaction() {
+ }
+
+ public Transaction(TransactionTypes type, TransactionStatus status, Double amount, String description, Account account) {
+ this.type = type;
+ this.status = status;
+ this.amount = amount;
+ this.description = description;
+ this.account = account;
+ this.timestamp = LocalDateTime.now();
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionStatus.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionStatus.java
new file mode 100644
index 0000000..f7a5c8e
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionStatus.java
@@ -0,0 +1,7 @@
+package org.bank.memory.entities.transactions;
+
+public enum TransactionStatus {
+ PENDING,
+ SUCCESS,
+ FAILED
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionTypes.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionTypes.java
new file mode 100644
index 0000000..dc1d656
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/transactions/TransactionTypes.java
@@ -0,0 +1,8 @@
+package org.bank.memory.entities.transactions;
+
+public enum TransactionTypes {
+ DEPOSIT,
+ WITHDRAW,
+ TRANSFER;
+}
+
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/users/HairColor.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/users/HairColor.java
new file mode 100644
index 0000000..a7af444
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/users/HairColor.java
@@ -0,0 +1,13 @@
+package org.bank.memory.entities.users;
+
+/**
+ * Перечисление для цвета волос.
+ */
+public enum HairColor {
+ BLONDE,
+ BROWN,
+ BLACK,
+ RED,
+ GRAY,
+ WHITE
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/entities/users/User.java b/bank-infrastructure/src/main/java/org/bank/memory/entities/users/User.java
new file mode 100644
index 0000000..029735d
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/entities/users/User.java
@@ -0,0 +1,97 @@
+package org.bank.memory.entities.users;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+import org.bank.memory.entities.accounts.Account;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Класс для создания пользователя.
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "users")
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "user_id")
+ private Long id;
+
+ @Column(name = "login", nullable = false, unique = true)
+ private String login;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private int age;
+
+ @Column(nullable = false)
+ private String gender;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "hair_color", nullable = false)
+ private HairColor hairColor;
+
+ @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List accounts = new ArrayList<>();
+
+ @ManyToMany
+ @JoinTable(
+ name = "user_friends",
+ joinColumns = @JoinColumn(name = "user_login"),
+ inverseJoinColumns = @JoinColumn(name = "friend_login")
+ )
+ private List friends = new ArrayList<>();
+
+ /**
+ * Конструктор для создания пользователя.
+ *
+ * @param login логин пользователя
+ * @param name имя пользователя
+ * @param age возраст пользователя
+ * @param gender пол пользователя
+ * @param hairColor цвет волос пользователя
+ */
+ public User(String login, String name, int age, String gender, HairColor hairColor) {
+
+ this.login = login;
+
+ this.name = name;
+
+ this.age = age;
+
+ this.gender = gender;
+
+ this.hairColor = hairColor;
+
+ this.friends = new ArrayList<>();
+ this.accounts = new ArrayList<>();
+ }
+
+ public User() {
+ }
+
+ /**
+ * Метод для добавления друга.
+ *
+ * @param friend экземпляр друга
+ */
+ public void addFriend(User friend) {
+ this.getFriends().add(friend);
+ }
+
+ /**
+ * Метод для удаления друга.
+ *
+ * @param friend друга
+ */
+ public void removeFriend(User friend) {
+ this.getFriends().remove(friend);
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/exceptions/AccountExceptions.java b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/AccountExceptions.java
new file mode 100644
index 0000000..0994da5
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/AccountExceptions.java
@@ -0,0 +1,31 @@
+package org.bank.memory.exceptions;
+
+/**
+ * Класс для создания исключений для счета пользователя.
+ */
+public class AccountExceptions extends Exception {
+
+ private AccountExceptions(String message) {
+ super(message);
+ }
+
+ public static AccountExceptions AmountIsNegativeException(String message) {
+ return new AccountExceptions(message);
+ }
+
+ public static AccountExceptions InsufficientFundsException(String message) {
+ return new AccountExceptions(message);
+ }
+
+ public static AccountExceptions AccountNotFoundException(String message) {
+ return new AccountExceptions(message);
+ }
+
+ public static AccountExceptions NoAccounts(String message) {
+ return new AccountExceptions(message);
+ }
+
+ public static AccountExceptions NoTransactionsException(String message) {
+ return new AccountExceptions(message);
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/exceptions/GlobalExceptionHandler.java b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/GlobalExceptionHandler.java
new file mode 100644
index 0000000..2725a89
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/GlobalExceptionHandler.java
@@ -0,0 +1,53 @@
+package org.bank.memory.exceptions;
+
+import org.bank.memory.DTO_entities.ErrorResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.WebRequest;
+
+import java.time.LocalDateTime;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(UserExceptions.class)
+ public ResponseEntity handleUserExceptions(UserExceptions ex, WebRequest request) {
+ HttpStatus status = determinedHttpStatus(ex);
+ ErrorResponse errorResponse = new ErrorResponse(
+ LocalDateTime.now(),
+ status.value(),
+ status.getReasonPhrase(),
+ ex.getMessage(),
+ request.getDescription(false).replace("uri=", "")
+ );
+ return new ResponseEntity<>(errorResponse, status);
+ }
+
+ @ExceptionHandler(AccountExceptions.class)
+ public ResponseEntity handleAccountExceptions(AccountExceptions ex, WebRequest request) {
+ HttpStatus status = determinedHttpStatus(ex);
+ ErrorResponse errorResponse = new ErrorResponse(
+ LocalDateTime.now(),
+ status.value(),
+ status.getReasonPhrase(),
+ ex.getMessage(),
+ request.getDescription(false).replace("uri=", "")
+ );
+ return new ResponseEntity<>(errorResponse, status);
+ }
+
+ private HttpStatus determinedHttpStatus(Exception ex) {
+ if (ex instanceof UserExceptions) {
+ return ex.getMessage().contains("не найден") ?
+ HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST;
+ } else if (ex instanceof AccountExceptions) {
+ return ex.getMessage().contains("не найден") ?
+ HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST;
+ }
+ return HttpStatus.INTERNAL_SERVER_ERROR;
+ }
+
+
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/exceptions/UserExceptions.java b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/UserExceptions.java
new file mode 100644
index 0000000..0463327
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/exceptions/UserExceptions.java
@@ -0,0 +1,18 @@
+package org.bank.memory.exceptions;
+
+/**
+ * Класс для создания исключений для пользователя.
+ */
+public class UserExceptions extends Exception {
+ private UserExceptions(String message) {
+ super(message);
+ }
+
+ public static UserExceptions UserNotFoundException(String message) {
+ return new UserExceptions(message);
+ }
+
+ public static UserExceptions UserAlreadyExistsException(String message) {
+ return new UserExceptions(message);
+ }
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/repos/AccountRepository.java b/bank-infrastructure/src/main/java/org/bank/memory/repos/AccountRepository.java
new file mode 100644
index 0000000..d69356e
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/repos/AccountRepository.java
@@ -0,0 +1,10 @@
+package org.bank.memory.repos;
+
+import org.bank.memory.entities.accounts.Account;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface AccountRepository extends JpaRepository {
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/repos/TransactionRepository.java b/bank-infrastructure/src/main/java/org/bank/memory/repos/TransactionRepository.java
new file mode 100644
index 0000000..6da96af
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/repos/TransactionRepository.java
@@ -0,0 +1,17 @@
+package org.bank.memory.repos;
+
+import org.bank.memory.entities.transactions.Transaction;
+import org.bank.memory.entities.transactions.TransactionTypes;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface TransactionRepository extends JpaRepository {
+ List findByAccountId(Long accountId);
+
+ List findTransactionByType(TransactionTypes type);
+
+ List findByAccountIdAndType(Long accountId, TransactionTypes type);
+}
\ No newline at end of file
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/repos/UserRepository.java b/bank-infrastructure/src/main/java/org/bank/memory/repos/UserRepository.java
new file mode 100644
index 0000000..fc0537c
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/repos/UserRepository.java
@@ -0,0 +1,21 @@
+package org.bank.memory.repos;
+
+import org.bank.memory.entities.users.HairColor;
+import org.bank.memory.entities.users.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+ List findByHairColorAndGender(HairColor hairColor, String gender);
+
+ List findByHairColor(HairColor hairColor);
+
+ List findByGender(String gender);
+
+ boolean existsByLogin(String login);
+
+ User findByLogin(String login);
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/CreateUserRequest.java b/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/CreateUserRequest.java
new file mode 100644
index 0000000..52abde1
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/CreateUserRequest.java
@@ -0,0 +1,16 @@
+package org.bank.memory.requestEntites;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class CreateUserRequest {
+ private String login;
+ private String name;
+ private int age;
+ private String gender;
+ private String hairColor;
+}
diff --git a/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/TransferRequestBody.java b/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/TransferRequestBody.java
new file mode 100644
index 0000000..a4ed060
--- /dev/null
+++ b/bank-infrastructure/src/main/java/org/bank/memory/requestEntites/TransferRequestBody.java
@@ -0,0 +1,16 @@
+package org.bank.memory.requestEntites;
+
+import org.springframework.lang.NonNull;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class TransferRequestBody {
+ private @NonNull Long fromAccountId;
+ private @NonNull Long toAccountId;
+ private double amount;
+}
diff --git a/bank-presentation/pom.xml b/bank-presentation/pom.xml
new file mode 100644
index 0000000..11a62e1
--- /dev/null
+++ b/bank-presentation/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+
+
+ bank-presentation
+
+
+ 23
+ 23
+ UTF-8
+
+
+
+ org.bank
+ bank-application
+ 1.0-SNAPSHOT
+ compile
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.1
+
+
+ org.bank
+ bank-infrastructure
+ 1.0-SNAPSHOT
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/bank-presentation/src/main/java/org/bank/present/BankApplication.java b/bank-presentation/src/main/java/org/bank/present/BankApplication.java
new file mode 100644
index 0000000..7375acb
--- /dev/null
+++ b/bank-presentation/src/main/java/org/bank/present/BankApplication.java
@@ -0,0 +1,23 @@
+package org.bank.present;
+
+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 = {
+ "org.bank.present",
+ "org.bank.core",
+ "org.bank.memory"
+})
+@EnableJpaRepositories(basePackages = "org.bank.memory.repos")
+@EntityScan(basePackages = {
+ "org.bank.memory.entities.accounts",
+ "org.bank.memory.entities.users",
+ "org.bank.memory.entities.transactions"
+})
+public class BankApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(BankApplication.class, args);
+ }
+}
diff --git a/bank-presentation/src/main/java/org/bank/present/controllers/AccountController.java b/bank-presentation/src/main/java/org/bank/present/controllers/AccountController.java
new file mode 100644
index 0000000..4eff8f8
--- /dev/null
+++ b/bank-presentation/src/main/java/org/bank/present/controllers/AccountController.java
@@ -0,0 +1,169 @@
+package org.bank.present.controllers;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import org.bank.core.services.AccountService;
+import org.bank.memory.DTO_entities.AccountDTO;
+import org.bank.memory.DTO_entities.TransactionDTO;
+import org.bank.memory.entities.transactions.TransactionTypes;
+import org.bank.memory.exceptions.AccountExceptions;
+import org.bank.memory.requestEntites.TransferRequestBody;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/accounts")
+@RequiredArgsConstructor
+public class AccountController {
+
+ private final AccountService accountService;
+
+ @Operation(summary = "Создание нового счёта")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "Счёт успешно создан\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при создании счёта\n")
+ })
+ @PostMapping("/create")
+ public ResponseEntity createAccount(@RequestBody String login) throws Exception {
+ AccountDTO accountDTO = accountService.createAccount(login);
+ return new ResponseEntity<>(accountDTO, HttpStatus.CREATED);
+ }
+
+ @Operation(summary = "Пополнение счета")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Счёт успешно пополнен\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при пополнении счета\n")
+ })
+ @PostMapping("/{id}/deposit")
+ public ResponseEntity deposit(@PathVariable("id") @NonNull Long id,
+ @RequestParam("amount") double amount)
+ throws Exception {
+ AccountDTO accountDTO = accountService.deposit(id, amount);
+ return new ResponseEntity<>(accountDTO, HttpStatus.OK);
+
+ }
+
+ @Operation(summary = "Снятие средств с счёта")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Средства сняты\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при снятии средств\n")
+ })
+ @PostMapping("/{accountId}/withdraw")
+ public ResponseEntity withdraw(@PathVariable("accountId") @NonNull Long accountId,
+ @RequestParam("amount") double amount)
+ throws Exception {
+ AccountDTO accountDTO = accountService.withdraw(accountId, amount);
+ return new ResponseEntity<>(accountDTO, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Перевод средств между счетами")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Перевод успешно выполнен\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при переводе средств\n")
+ })
+ @PostMapping("/transfer")
+ public ResponseEntity> transfer(TransferRequestBody transferRequestBody)
+ throws Exception {
+ ArrayList accountDTOS = accountService.transfer(transferRequestBody);
+ return new ResponseEntity<>(accountDTOS, HttpStatus.OK);
+
+ }
+
+ @Operation(summary = "Получить баланс счета")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Баланс успешно получен\n"),
+ @ApiResponse(responseCode = "404", description = "Счёт не найден\n")
+ })
+ @GetMapping("/{accountId}/balance")
+ public ResponseEntity getBalance(@PathVariable("accountId") @NonNull Long accountId)
+ throws AccountExceptions {
+ AccountDTO accountDTO = accountService.checkBalance(accountId);
+ return new ResponseEntity<>(accountDTO, HttpStatus.OK);
+
+ }
+
+ /**
+ * Получение всех счетов системы.
+ *
+ * @return ResponseEntity со списком всех счетов
+ */
+ @Operation(summary = "Получить все счета системы")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Все счета успешно получены\n"),
+ @ApiResponse(responseCode = "404", description = "Счета не найдены\n")
+ })
+ @GetMapping
+ public ResponseEntity> getAllAccounts() throws AccountExceptions {
+ List accountDTOs = accountService.getAllAccounts();
+ return new ResponseEntity<>(accountDTOs, HttpStatus.OK);
+ }
+
+ /**
+ * Получение всех операций.
+ */
+ @Operation(summary = "Получить все операции")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Операции успешно получены\n"),
+ @ApiResponse(responseCode = "404", description = "Операции не найдены\n")
+ })
+ @GetMapping("/transactions")
+ public ResponseEntity> getAllTransactions() throws AccountExceptions {
+ List transactionDTOs = accountService.getAllTransactions();
+ return new ResponseEntity<>(transactionDTOs, HttpStatus.OK);
+ }
+
+ /**
+ * Получение всех операций с фильтрацией по типу и accountId.
+ *
+ * @param accountId идентификатор счета
+ * @return ResponseEntity со списком операций
+ */
+ @Operation(summary = "Получить все операции с фильтрацией по accountId")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Операции успешно получены\n"),
+ @ApiResponse(responseCode = "404", description = "Операции не найдены\n")
+ })
+ @GetMapping("/{account_id}/transactions")
+ public ResponseEntity> getAllTransactionsById(@PathVariable("account_id") Long accountId)
+ throws AccountExceptions {
+ List transactionDTOs = accountService.getAllTransactionsById(accountId);
+ return new ResponseEntity<>(transactionDTOs, HttpStatus.OK);
+ }
+
+ /**
+ * Получение всех операций с фильтрацией по типу.
+ * \
+ *
+ * @param transactionTypes тип операции
+ * @return ResponseEntity со списком операций
+ */
+ @Operation(summary = "Получить все операции с фильтрацией по типу")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Операции успешно получены\n"),
+ @ApiResponse(responseCode = "404", description = "Операции не найдены\n")
+ })
+ @GetMapping("/transactions/{type}")
+ public ResponseEntity> getAllTransactionsByType(
+ @PathVariable("type") TransactionTypes transactionTypes)
+ throws AccountExceptions {
+ List transactionDTOs = accountService.getAllTransactionsByType(transactionTypes);
+ return new ResponseEntity<>(transactionDTOs, HttpStatus.OK);
+ }
+
+ @GetMapping("/{account_id}/transactions/{type}")
+ public ResponseEntity> getAllTransactionsByIdAndType(
+ @PathVariable("account_id") Long accountId,
+ @PathVariable("type") TransactionTypes transactionTypes)
+ throws AccountExceptions {
+ List transactionDTOs = accountService.getAllTransactionsByIdAndType(accountId,
+ transactionTypes);
+ return new ResponseEntity<>(transactionDTOs, HttpStatus.OK);
+ }
+}
diff --git a/bank-presentation/src/main/java/org/bank/present/controllers/UserController.java b/bank-presentation/src/main/java/org/bank/present/controllers/UserController.java
new file mode 100644
index 0000000..a53f292
--- /dev/null
+++ b/bank-presentation/src/main/java/org/bank/present/controllers/UserController.java
@@ -0,0 +1,153 @@
+package org.bank.present.controllers;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import org.bank.core.services.UserService;
+import org.bank.memory.DTO_entities.AccountDTO;
+import org.bank.memory.DTO_entities.UserDTO;
+import org.bank.memory.entities.users.HairColor;
+import org.bank.memory.exceptions.UserExceptions;
+import org.bank.memory.requestEntites.CreateUserRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/users")
+@RequiredArgsConstructor
+public class UserController {
+ private final UserService userService;
+
+ @Operation(summary = "Регистрация нового пользователя")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "Пользователь успешно зарегистрирован\n"),
+ @ApiResponse(responseCode = "400", description = "Пользователь с таким логином уже существует\n")
+ })
+ @PostMapping
+ public ResponseEntity registerUser(@RequestBody CreateUserRequest createUserRequest)
+ throws Exception {
+ UserDTO userDTO = userService.registerUser(createUserRequest);
+ return ResponseEntity.status(HttpStatus.CREATED).body(userDTO);
+ }
+
+ @Operation(summary = "Получить информацию о пользователе по логину")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Информация о пользователе\n"),
+ @ApiResponse(responseCode = "404", description = "Пользователь не найден\n")
+ })
+ @GetMapping("/{login}")
+ public ResponseEntity getUserInfo(@PathVariable("login") String login)
+ throws UserExceptions {
+ UserDTO userDTO = userService.getUserInfo(login);
+ return new ResponseEntity<>(userDTO, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Добавить друга")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Друг успешно добавлен\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при добавлении друга\n"),
+ @ApiResponse(responseCode = "404", description = "Пользователь или друг не найден\n")
+ })
+ @PostMapping("/{user_id}/add_friend/{friend_id}")
+ public ResponseEntity> addFriend(@PathVariable("user_id") @NonNull Long user_id,
+ @PathVariable("friend_id") @NonNull Long friend_id)
+ throws Exception {
+ ArrayList userDTOS = userService.addFriend(user_id, friend_id);
+ return new ResponseEntity<>(userDTOS, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Удалить друга у пользователя")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Друг успешно удалён\n"),
+ @ApiResponse(responseCode = "400", description = "Ошибка при удалении друга\n"),
+ @ApiResponse(responseCode = "404", description = "Пользователь или друг не найден\n")
+ })
+ @DeleteMapping("/{user_id}/delete_friend/{friend_id}")
+ public ResponseEntity> deleteFriend(@PathVariable("user_id") @NonNull Long user_id,
+ @PathVariable("friend_id") @NonNull Long friend_id)
+ throws UserExceptions {
+ ArrayList userDTOS = userService.deleteFriend(user_id, friend_id);
+ return new ResponseEntity<>(userDTOS, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Получить все счёта пользователя")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Счета пользователя\n"),
+ @ApiResponse(responseCode = "404", description = "Пользователь не найден\n")
+ })
+ @GetMapping("/{userid}/accounts")
+ public ResponseEntity> getUserAccounts(@PathVariable("userid") @NonNull Long userid)
+ throws UserExceptions {
+ List accounts = userService.getUserAccounts(userid);
+ return new ResponseEntity<>(accounts, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Получить всех друзей пользователя")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Друзья пользователя\n"),
+ @ApiResponse(responseCode = "404", description = "Пользователь не найден\n")
+ })
+ @GetMapping("/{userid}/friends")
+ public ResponseEntity> getUserFriends(@PathVariable @NonNull Long userid)
+ throws UserExceptions {
+ List friends = userService.getUserFriends(userid);
+ return new ResponseEntity<>(friends, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Получить всех пользователей")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Существующие пользователи: \n"),
+ @ApiResponse(responseCode = "404", description = "Пользователи не найдены\n")
+ })
+ @GetMapping
+ public ResponseEntity> getAllUsers() throws UserExceptions {
+ List users = userService.getAll();
+ return new ResponseEntity<>(users, HttpStatus.OK);
+ }
+
+ @GetMapping("/{haircolor}/{gender}")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Пользователи с фильтрацией " +
+ "по цвету волос и полу: \n"),
+ @ApiResponse(responseCode = "404", description = "Пользователи не найдены\n")
+ })
+ public ResponseEntity> getAllUsersFilteredByHairColorAndGender(
+ @PathVariable("haircolor") String hairColor,
+ @PathVariable("gender") String gender) throws UserExceptions {
+ HairColor hairColorEnum = HairColor.valueOf(hairColor.toUpperCase());
+ List userDTOs = userService.getFilteredUsers(hairColorEnum, gender);
+ return new ResponseEntity<>(userDTOs, HttpStatus.OK);
+ }
+
+ @GetMapping("/{haircolor}/")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Пользователи с фильтрацией " +
+ "по цвету волос: \n"),
+ @ApiResponse(responseCode = "404", description = "Пользователи не найдены\n")
+ })
+ public ResponseEntity> getAllUsersFilteredByHairColor(
+ @PathVariable("haircolor") String haircolor) throws UserExceptions {
+ HairColor hairColorEnum = HairColor.valueOf(haircolor.toUpperCase());
+ List users = userService.getFilteredUsersByHairColor(hairColorEnum);
+
+ return new ResponseEntity<>(users, HttpStatus.OK);
+ }
+
+ @GetMapping("/{gender}/")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Пользователи с фильтрацией " +
+ "по полу: \n"),
+ @ApiResponse(responseCode = "404", description = "Пользователи не найдены\n")
+ })
+ public ResponseEntity> getAllUsersFilteredByGender(@PathVariable String gender)
+ throws UserExceptions {
+ List users = userService.getFilteredUsersByGender(gender);
+ return new ResponseEntity<>(users, HttpStatus.OK);
+ }
+}
diff --git a/bank-presentation/src/main/resources/application.properties b/bank-presentation/src/main/resources/application.properties
new file mode 100644
index 0000000..c0816e1
--- /dev/null
+++ b/bank-presentation/src/main/resources/application.properties
@@ -0,0 +1,19 @@
+
+server.port=8081
+
+spring.datasource.url=jdbc:postgresql://localhost:54321/bank
+spring.datasource.username=postgres
+spring.datasource.password=postgres
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+
+springdoc.api-docs.enabled=true
+springdoc.swagger-ui.enabled=true
+
+logging.level.org.springframework=INFO
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..b129c69
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+services:
+ postgres:
+ container_name: postgres
+ image: postgres:14.5
+ hostname: postgres
+ restart: unless-stopped
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ - "54321:5432"
+ environment:
+ - POSTGRES_DB=bank
+ - POSTGRES_PORT=5432
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_USER=postgres
+
+ postgres-web:
+ depends_on:
+ - postgres
+ container_name: postgres_web
+ image: dpage/pgadmin4:latest
+ hostname: postgres-web
+ restart: unless-stopped
+ ports:
+ - "8080:80"
+ environment:
+ - PGADMIN_DEFAULT_PASSWORD=postgres
+ - PGADMIN_DEFAULT_EMAIL=postgres@postgres.com
+ - PGADMIN_CONFIG_SERVER_MODE=False
+ - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False
+
+volumes:
+ postgres_data:
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..fb3c3fb
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,165 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.4
+
+
+
+ org.bank
+ bank-parent
+ 1.0-SNAPSHOT
+ pom
+
+
+ bank-application
+ bank-infrastructure
+ bank-presentation
+ API-Gateway
+ Storage
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.6
+
+
+
+ org.mapstruct
+ mapstruct
+ 1.6.3
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.3
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.1.1
+
+
+
+ jakarta.xml.bind
+ jakarta.xml.bind-api
+ 4.0.2
+
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+ 4.0.5
+
+
+
+ io.swagger.core.v3
+ swagger-core
+ 2.2.30
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+ 3.4.4
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ 3.4.4
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ 3.4.4
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.38
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+ 3.4.4
+
+
+
+ org.mockito
+ mockito-core
+ 5.16.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.16.0
+ test
+
+
+
+ org.postgresql
+ postgresql
+ 42.7.5
+
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.1
+
+
+
+
+ 23
+ 23
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.3
+
+
+ org.projectlombok
+ lombok
+ 1.18.32
+
+
+ true
+
+
+
+
+
+