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 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 + + + + + +