diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98c3242..fe71e51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,27 +66,4 @@ jobs: name: ledger-jar path: target/LedgerSystem-0.0.1-SNAPSHOT.jar - # ============================================ - # JOB 3: Build Docker Image (Optional) - # ============================================ - build-docker: - name: 🐳 Build Docker Image - runs-on: ubuntu-latest - needs: [unit-tests, build] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - steps: - - name: 📥 Checkout code - uses: actions/checkout@v3 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: 🐳 Build Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: false - tags: ledger-system:${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/src/main/java/com/example/ledgersystem/DataSeeder.java b/src/main/java/com/example/ledgersystem/DataSeeder.java index ef8ef35..eec6b64 100644 --- a/src/main/java/com/example/ledgersystem/DataSeeder.java +++ b/src/main/java/com/example/ledgersystem/DataSeeder.java @@ -8,7 +8,7 @@ import com.example.ledgersystem.repositories.RoleRepository; import com.example.ledgersystem.repositories.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -19,6 +19,7 @@ @Component // <--- Tells Spring to manage this class @RequiredArgsConstructor +@Slf4j public class DataSeeder implements CommandLineRunner { private final PasswordEncoder passwordEncoder; @@ -33,17 +34,21 @@ public void run(String... args) throws Exception { // 1. Check if data already exists so we don't duplicate every restart if (accountRepository.count() > 0) { - System.out.println("✅ Database already seeded. Skipping..."); + log.info("Database already seeded. Skipping data initialization."); return; } + log.info("Starting database seed..."); + Role userRole = roleRepository .findByRoleName(AppRoles.ROLE_USER) .orElseGet(() -> roleRepository.save(new Role(AppRoles.ROLE_USER))); + log.debug("Role created/found: {}", AppRoles.ROLE_USER); Role adminRole = roleRepository .findByRoleName(AppRoles.ROLE_ADMIN) .orElseGet(() -> roleRepository.save(new Role(AppRoles.ROLE_ADMIN))); + log.debug("Role created/found: {}", AppRoles.ROLE_ADMIN); User systemAdmin = new User(); systemAdmin.setUsername("systemAdmin"); @@ -51,6 +56,7 @@ public void run(String... args) throws Exception { systemAdmin.setRole(List.of(userRole, adminRole)); systemAdmin.setEmail("system@gmail.com"); userRepository.save(systemAdmin); + log.debug("System admin user created: username=systemAdmin"); Account systemAccount = new Account(); systemAccount.setName("CENTRAL_BANK"); @@ -58,6 +64,7 @@ public void run(String... args) throws Exception { systemAccount.setCurrency("INR"); systemAccount.setUser(systemAdmin); accountRepository.save(systemAccount); + log.debug("CENTRAL_BANK account created: accountId={}", systemAccount.getAccountId()); // ---- CREATE USERS ---- User aliceUser = new User(); @@ -79,6 +86,7 @@ public void run(String... args) throws Exception { charlieUser.setRole(List.of(userRole)); userRepository.saveAll(List.of(aliceUser, bobUser, charlieUser)); + log.debug("Test users created: alice, bob, charlie"); // 2. Create Dummy Accounts Account alice = new Account(); @@ -102,13 +110,7 @@ public void run(String... args) throws Exception { // 3. Save to DB accountRepository.saveAll(Arrays.asList(alice, bob, charlie)); - // 4. PRINT UUIDs (Critical for Testing!) - System.out.println("--------------------------------------------"); - System.out.println("🎉 DATA SEEDED SUCCESSFULLY"); - System.out.println("--------------------------------------------"); - System.out.println("👤 Alice UUID: " + alice.getAccountId()); - System.out.println("👤 Bob UUID: " + bob.getAccountId()); - System.out.println("👤 Charlie UUID: " + charlie.getAccountId()); - System.out.println("--------------------------------------------"); + log.info("Database seeded successfully. Accounts: alice={}, bob={}, charlie={}", + alice.getAccountId(), bob.getAccountId(), charlie.getAccountId()); } } diff --git a/src/main/java/com/example/ledgersystem/Exceptions/MyGlobalExceptionHandler.java b/src/main/java/com/example/ledgersystem/Exceptions/MyGlobalExceptionHandler.java index 1e98281..84ac91a 100644 --- a/src/main/java/com/example/ledgersystem/Exceptions/MyGlobalExceptionHandler.java +++ b/src/main/java/com/example/ledgersystem/Exceptions/MyGlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.example.ledgersystem.Exceptions; import com.example.ledgersystem.Payloads.ApiResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; @@ -12,6 +13,7 @@ import java.util.Map; @RestControllerAdvice +@Slf4j public class MyGlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @@ -24,12 +26,14 @@ public ResponseEntity> myMethodArgumentNotValidException(Met String message = err.getDefaultMessage(); response.put(fieldName, message); }); + log.warn("Validation failed: fields={}", response.keySet()); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } @ExceptionHandler(AccountNotFound.class) //Our own excep public ResponseEntity ResNotFound(AccountNotFound e) { + log.warn("Account not found: {}", e.getMessage()); String Message = e.getMessage(); //It's in parent class RunTimeExcep that's why we used super ApiResponse apires = new ApiResponse(Message, false); return new ResponseEntity<>(apires, HttpStatus.NOT_FOUND); @@ -37,6 +41,7 @@ public ResponseEntity ResNotFound(AccountNotFound e) { @ExceptionHandler(APIexception.class) //Our own excep If you try to create an already existing category public ResponseEntity myapiexcep(APIexception e) { + log.warn("API exception: {}", e.getMessage()); String Message = e.getMessage(); //It's in parent class RunTimeExcep that's why we used super ApiResponse apires = new ApiResponse(Message, false); return new ResponseEntity<>(apires, HttpStatus.BAD_REQUEST); @@ -44,6 +49,7 @@ public ResponseEntity myapiexcep(APIexception e) { @ExceptionHandler(DuplicateTransactionException.class) //Our own excep If you try to create an already existing category public ResponseEntity duplicTransac(DuplicateTransactionException e) { + log.warn("Duplicate transaction: {}", e.getMessage()); String Message = e.getMessage(); //It's in parent class RunTimeExcep that's why we used super ApiResponse apires = new ApiResponse(Message, false); return new ResponseEntity<>(apires, HttpStatus.CONFLICT); @@ -51,6 +57,7 @@ public ResponseEntity duplicTransac(DuplicateTransactionException e @ExceptionHandler(InsufficientFundsException.class) //Our own excep If you try to create an already existing category public ResponseEntity insufFunda(InsufficientFundsException e) { + log.warn("Insufficient funds: {}", e.getMessage()); String Message = e.getMessage(); //It's in parent class RunTimeExcep that's why we used super ApiResponse apires = new ApiResponse(Message, false); return new ResponseEntity<>(apires, HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/example/ledgersystem/Security/Services/UserDetailsServiceImpl.java b/src/main/java/com/example/ledgersystem/Security/Services/UserDetailsServiceImpl.java index dc070a7..463ded0 100644 --- a/src/main/java/com/example/ledgersystem/Security/Services/UserDetailsServiceImpl.java +++ b/src/main/java/com/example/ledgersystem/Security/Services/UserDetailsServiceImpl.java @@ -2,6 +2,7 @@ import com.example.ledgersystem.model.User; import com.example.ledgersystem.repositories.UserRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -9,6 +10,7 @@ import org.springframework.stereotype.Service; @Service +@Slf4j public class UserDetailsServiceImpl implements UserDetailsService { //Calls DAO and encoder type shit @Autowired @@ -16,9 +18,13 @@ public class UserDetailsServiceImpl implements UserDetailsService { //Calls DAO @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + log.debug("Loading user by username: {}", username); User user = userRepository.findByUsername(username) - .orElseThrow(() -> - new UsernameNotFoundException("User not found with given username!!!")); + .orElseThrow(() -> { + log.warn("User not found: username={}", username); + return new UsernameNotFoundException("User not found with given username!!!"); + }); + log.debug("User loaded successfully: username={}", username); return UserDetailsImpl.build(user); //Builds the container which stores user data } } diff --git a/src/main/java/com/example/ledgersystem/Security/jwt/AuthEntryPointJwt.java b/src/main/java/com/example/ledgersystem/Security/jwt/AuthEntryPointJwt.java index 386cc86..67c50f0 100644 --- a/src/main/java/com/example/ledgersystem/Security/jwt/AuthEntryPointJwt.java +++ b/src/main/java/com/example/ledgersystem/Security/jwt/AuthEntryPointJwt.java @@ -4,8 +4,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -16,14 +15,14 @@ import java.util.Map; @Component +@Slf4j public class AuthEntryPointJwt implements AuthenticationEntryPoint { - private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - logger.error("Authentication Failed: {}",authException.getMessage()); + log.warn("Unauthorized access attempt: path={}, reason={}", request.getServletPath(), authException.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); final Map body = new HashMap<>(); diff --git a/src/main/java/com/example/ledgersystem/Security/jwt/AuthTokenFilter.java b/src/main/java/com/example/ledgersystem/Security/jwt/AuthTokenFilter.java index 2c56407..c86e6ef 100644 --- a/src/main/java/com/example/ledgersystem/Security/jwt/AuthTokenFilter.java +++ b/src/main/java/com/example/ledgersystem/Security/jwt/AuthTokenFilter.java @@ -6,8 +6,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -18,6 +17,7 @@ import java.io.IOException; @Component +@Slf4j public class AuthTokenFilter extends OncePerRequestFilter { @Autowired @@ -25,29 +25,32 @@ public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; - private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); - @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try{ String jwt = parseJwt(request); //Extracting token - if(jwt!=null && jwtUtils.validateToken(jwt)){ //Validating token - String username = jwtUtils.getUserNameFromToken(jwt); //Extracting user - UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsServiceImpl.loadUserByUsername(username); //Loading userdetails from DB to create a new auth obj + if(jwt != null){ + log.debug("JWT token found in request: path={}", request.getServletPath()); + if(jwtUtils.validateToken(jwt)){ //Validating token + String username = jwtUtils.getUserNameFromToken(jwt); //Extracting user + log.debug("Valid JWT received for user: {}", username); + UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsServiceImpl.loadUserByUsername(username); //Loading userdetails from DB to create a new auth obj /*This is the child class of actual auth obj*/UsernamePasswordAuthenticationToken authentication = //creating container/auth obj which stores usernamepass and roles - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities() - ); - authentication.setDetails( //Adding all request details(ip address) to authetication obj - new WebAuthenticationDetailsSource().buildDetails(request)); + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + authentication.setDetails( //Adding all request details(ip address) to authetication obj + new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("User authenticated: {} with authorities={}", username, userDetails.getAuthorities()); + } } } catch (Exception e) { - logger.error("Cannot set user authentication: {}", e.getMessage()); + log.error("Cannot set user authentication: {}", e.getMessage()); } filterChain.doFilter(request, response); //Telling spring to continue with ita in built filters } diff --git a/src/main/java/com/example/ledgersystem/Security/jwt/JwtUtils.java b/src/main/java/com/example/ledgersystem/Security/jwt/JwtUtils.java index 6aa210d..c00b164 100644 --- a/src/main/java/com/example/ledgersystem/Security/jwt/JwtUtils.java +++ b/src/main/java/com/example/ledgersystem/Security/jwt/JwtUtils.java @@ -9,7 +9,7 @@ import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -21,8 +21,8 @@ @Component +@Slf4j public class JwtUtils { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtUtils.class); @Value("${spring.app.jwtExpirationMs}") private long jwtExpirationMs; @@ -38,7 +38,7 @@ public class JwtUtils { //FOR SWAGGER AS VO COOKIE NHI SAMJHTA public String getJwtFromHeader(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); - logger.debug("Authorization Header: {}",bearerToken); + log.debug("Authorization Header: {}", bearerToken); if(bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); //Remove bearer prefix } @@ -56,6 +56,7 @@ public String getJwtFromCookies(HttpServletRequest request) { //Used in Au public ResponseCookie generateJwtCookie(UserDetailsImpl userDetails) { //Used in sign in String jwt = generateTokenFromUsername(userDetails); + log.debug("JWT cookie generated for user: {}", userDetails.getUsername()); ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt) .path("/api") //Valid within this .maxAge(24*60*60) @@ -74,6 +75,7 @@ public ResponseCookie getCleanCookie() { //Used in sign in //Generate token from username public String generateTokenFromUsername(UserDetailsImpl userDetails) { String username = userDetails.getUsername(); + log.debug("Generating JWT token for user: {}", username); return Jwts.builder() .subject(username) //setting data .issuedAt(new Date()) @@ -100,20 +102,20 @@ public Key key(){ //Validate JWT Token public boolean validateToken(String token) { try{ - System.out.println("Validate"); + log.debug("Validating JWT token"); Jwts.parser() .verifyWith((SecretKey) key()) .build() .parseSignedClaims(token); return true; }catch(MalformedJwtException exception){ - logger.info("Invalid token: {}", exception.getMessage()); + log.warn("Invalid JWT token: {}", exception.getMessage()); } catch (ExpiredJwtException e){ - logger.error("ExpiredJwtException: {}", e.getMessage()); + log.warn("JWT token expired: {}", e.getMessage()); } catch (UnsupportedJwtException e){ - logger.error("UnsupportedJwtException: {}", e.getMessage()); + log.warn("Unsupported JWT token: {}", e.getMessage()); } catch (IllegalArgumentException e){ - logger.error("IllegalArgumentException: {}", e.getMessage()); + log.warn("JWT claims string is empty: {}", e.getMessage()); } return false; } diff --git a/src/main/java/com/example/ledgersystem/controller/AccountController.java b/src/main/java/com/example/ledgersystem/controller/AccountController.java index 85d04b4..fbfdf6a 100644 --- a/src/main/java/com/example/ledgersystem/controller/AccountController.java +++ b/src/main/java/com/example/ledgersystem/controller/AccountController.java @@ -15,6 +15,7 @@ import com.example.ledgersystem.utils.AuthUtils; import io.github.bucket4j.Bucket; // Import Bucket import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -26,6 +27,7 @@ @RestController @RequestMapping("/api") +@Slf4j public class AccountController { @Autowired @@ -50,9 +52,13 @@ private boolean isRateLimited(UUID userId, RateLimitType type) { @PostMapping("/transfer") public ResponseEntity transferMoney(@Valid @RequestBody MoneyTransferDTO moneyTransferDTO) { UUID userId = authUtils.loggedInUserId(); + log.info("Transfer API called: fromAccount={}, toAccount={}, amount={}, reference={}, user={}", + moneyTransferDTO.getFromAccountId(), moneyTransferDTO.getToAccountId(), + moneyTransferDTO.getAmount(), moneyTransferDTO.getReferenceId(), userId); // 🛡️ SHIELD if (isRateLimited(userId, RateLimitType.TRANSACTION)) { + log.warn("Rate limit exceeded: userId={}, type=TRANSACTION, endpoint=/api/transfer", userId); return new ResponseEntity<>( new ApiResponse("Too many requests! Please wait a minute.", false), HttpStatus.TOO_MANY_REQUESTS @@ -66,6 +72,7 @@ public ResponseEntity transferMoney(@Valid @RequestBody MoneyTransf moneyTransferDTO.getReferenceId(), userId ); + log.info("Transfer API completed: reference={}, user={}", moneyTransferDTO.getReferenceId(), userId); return new ResponseEntity<>(apiResponse, HttpStatus.OK); } @@ -77,9 +84,11 @@ public ResponseEntity getStatement(@PathVariable("accountId") UUID accountId, @RequestParam(name = "sortBy", defaultValue = "loggedAt", required = false) String sortBy, @RequestParam(name = "sortOrder", defaultValue = AppConst.SORT_ORDER, required = false) String sortOrder) { UUID userId = authUtils.loggedInUserId(); + log.info("Statement API called: accountId={}, pageNumber={}, pageSize={}, user={}", accountId, pageNumber, pageSize, userId); // 🛡️ SHIELD if (isRateLimited(userId, RateLimitType.GENERAL)) { + log.warn("Rate limit exceeded: userId={}, type=GENERAL, endpoint=/api/statement/{}", userId, accountId); return new ResponseEntity<>( new ApiResponse("Too many requests! Please wait a minute.", false), HttpStatus.TOO_MANY_REQUESTS @@ -87,15 +96,20 @@ public ResponseEntity getStatement(@PathVariable("accountId") UUID accountId, } StatementResponse statementResponse = accountService.accountStatement(accountId, pageNumber, pageSize, sortBy, sortOrder, userId); + log.debug("Statement API completed: accountId={}, user={}", accountId, userId); return new ResponseEntity<>(statementResponse, HttpStatus.OK); } @PostMapping("/deposit") public ResponseEntity deposit(@Valid @RequestBody DepositRequestDTO depositRequestDTO) { UUID userId = authUtils.loggedInUserId(); + log.info("Deposit API called: toAccount={}, amount={}, reference={}, user={}", + depositRequestDTO.getToAccountId(), depositRequestDTO.getAmount(), + depositRequestDTO.getReferenceId(), userId); // 🛡️ SHIELD if (isRateLimited(userId, RateLimitType.TRANSACTION)) { + log.warn("Rate limit exceeded: userId={}, type=TRANSACTION, endpoint=/api/deposit", userId); return new ResponseEntity<>( new ApiResponse("Too many requests! Please wait a minute.", false), HttpStatus.TOO_MANY_REQUESTS @@ -108,15 +122,20 @@ public ResponseEntity deposit(@Valid @RequestBody DepositRequestDTO depositRequestDTO.getReferenceId(), userId ); + log.info("Deposit API completed: reference={}, user={}", depositRequestDTO.getReferenceId(), userId); return new ResponseEntity<>(apiResponse, HttpStatus.OK); } @PostMapping("/withdraw") public ResponseEntity withdraw(@Valid @RequestBody WithdrawRequestDTO withdrawRequestDTO) { UUID userId = authUtils.loggedInUserId(); + log.info("Withdraw API called: fromAccount={}, amount={}, reference={}, user={}", + withdrawRequestDTO.getFromAccountId(), withdrawRequestDTO.getAmount(), + withdrawRequestDTO.getReferenceId(), userId); // 🛡️ SHIELD if (isRateLimited(userId, RateLimitType.TRANSACTION)) { + log.warn("Rate limit exceeded: userId={}, type=TRANSACTION, endpoint=/api/withdraw", userId); return new ResponseEntity<>( new ApiResponse("Too many requests! Please wait a minute.", false), HttpStatus.TOO_MANY_REQUESTS @@ -125,6 +144,7 @@ public ResponseEntity withdraw(@Valid @RequestBody WithdrawRequestD Optional toAccount = accountRepository.findByName("CENTRAL_BANK"); if (toAccount.isEmpty()) { + log.error("System error: CENTRAL_BANK account not found during withdrawal, reference={}", withdrawRequestDTO.getReferenceId()); throw new APIexception("System Error: Central Bank Vault missing"); } @@ -135,6 +155,7 @@ public ResponseEntity withdraw(@Valid @RequestBody WithdrawRequestD withdrawRequestDTO.getReferenceId(), userId ); + log.info("Withdraw API completed: reference={}, user={}", withdrawRequestDTO.getReferenceId(), userId); return new ResponseEntity<>(apiResponse, HttpStatus.OK); } @@ -142,9 +163,11 @@ public ResponseEntity withdraw(@Valid @RequestBody WithdrawRequestD @GetMapping("/balance/{accountId}") public ResponseEntity getBalance(@PathVariable UUID accountId) { UUID authenticatedUserId = authUtils.loggedInUserId(); + log.info("Balance API called: accountId={}, user={}", accountId, authenticatedUserId); // 🛡️ SHIELD if (isRateLimited(authenticatedUserId, RateLimitType.GENERAL)) { + log.warn("Rate limit exceeded: userId={}, type=GENERAL, endpoint=/api/balance/{}", authenticatedUserId, accountId); return new ResponseEntity<>( new ApiResponse("Too many requests! Please wait a minute.", false), HttpStatus.TOO_MANY_REQUESTS @@ -152,6 +175,7 @@ public ResponseEntity getBalance(@PathVariable UUID accountId) { } BigDecimal balance = accountService.getBalance(accountId, authenticatedUserId); + log.debug("Balance API completed: accountId={}, user={}", accountId, authenticatedUserId); return new ResponseEntity<>(balance, HttpStatus.OK); } } diff --git a/src/main/java/com/example/ledgersystem/controller/AdminController.java b/src/main/java/com/example/ledgersystem/controller/AdminController.java index 00bfb18..9f22f5a 100644 --- a/src/main/java/com/example/ledgersystem/controller/AdminController.java +++ b/src/main/java/com/example/ledgersystem/controller/AdminController.java @@ -4,6 +4,7 @@ import com.example.ledgersystem.repositories.AccountRepository; import com.example.ledgersystem.repositories.LedgerEntryRepository; import com.example.ledgersystem.service.AdminService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -16,6 +17,7 @@ @RestController @RequestMapping("/api/admin") +@Slf4j public class AdminController { @Autowired private AdminService adminService; @@ -23,7 +25,9 @@ public class AdminController { @PreAuthorize("hasAuthority('ROLE_ADMIN')") @GetMapping("/audit/{accountId}") ResponseEntity audit(@PathVariable UUID accountId){ + log.info("Admin audit API called: accountId={}", accountId); Auditresponse auditresponse = adminService.audit(accountId); + log.info("Admin audit API completed: accountId={}, result={}", accountId, auditresponse.getStatus()); return ResponseEntity.ok(auditresponse); } diff --git a/src/main/java/com/example/ledgersystem/controller/AuthController.java b/src/main/java/com/example/ledgersystem/controller/AuthController.java index f17ebd9..46c7bce 100644 --- a/src/main/java/com/example/ledgersystem/controller/AuthController.java +++ b/src/main/java/com/example/ledgersystem/controller/AuthController.java @@ -9,6 +9,7 @@ import com.example.ledgersystem.model.User; import com.example.ledgersystem.repositories.UserRepository; import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -31,6 +32,7 @@ @RestController @RequestMapping("/api/auth") +@Slf4j public class AuthController { @Autowired @@ -48,12 +50,14 @@ public class AuthController { @PostMapping("/signin") public ResponseEntity authenticateUser(@RequestBody LoginRequestDTO loginRequestDTO){ + log.info("Sign-in attempt: username={}", loginRequestDTO.getUsername()); Authentication authentication; //auth obj try{ authentication = authenticationManager.authenticate( //.authenticate will check the username and password with the obj provided, and then it will load auth obj with userdetails if correct new UsernamePasswordAuthenticationToken(loginRequestDTO.getUsername(), loginRequestDTO.getPassword()) //Usernamepasstokken is used to describe username pass ); }catch(AuthenticationException e){ + log.warn("Sign-in failed: username={}, reason={}", loginRequestDTO.getUsername(), e.getMessage()); Map map = new HashMap<>(); map.put("message", "Invalid username or password"); map.put("status", false); @@ -70,21 +74,25 @@ public ResponseEntity authenticateUser(@RequestBody LoginRequestDTO loginRequ assert userDetails != null; UserInfoResponse response = new UserInfoResponse(userDetails.getId(), userDetails.getUsername(), tokenString); + log.info("Sign-in successful: username={}, userId={}", userDetails.getUsername(), userDetails.getId()); return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(response); } @PostMapping("/signup") public ResponseEntity registerUser(@Valid @RequestBody SignUpRequest signUpRequest){ + log.info("Sign-up attempt: username={}, email={}", signUpRequest.getUsername(), signUpRequest.getEmail()); //Checking for already existing account //Checking for already existing account if(userRepository.existsByUsername(signUpRequest.getUsername())){ + log.warn("Sign-up rejected: username already taken, username={}", signUpRequest.getUsername()); // FIX: Send the object, not the string return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!")); } if(userRepository.existsByEmail(signUpRequest.getEmail())){ + log.warn("Sign-up rejected: email already in use, email={}", signUpRequest.getEmail()); // FIX: Send the object, not the string return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); } @@ -96,6 +104,7 @@ public ResponseEntity registerUser(@Valid @RequestBody SignUpRequest signUpRe signUpRequest.getEmail() ); userRepository.save(user); + log.info("Sign-up successful: username={}, email={}", signUpRequest.getUsername(), signUpRequest.getEmail()); return ResponseEntity.ok(new MessageResponse("User registered successfully!")); } @@ -122,6 +131,7 @@ public ResponseEntity getUserDetails(Authentication authentication){ @PostMapping("/signout") public ResponseEntity logoutUser(){ + log.info("Sign-out requested"); ResponseCookie cookie = jwtUtils.getCleanCookie(); return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(new MessageResponse("Successfully logged out!")); diff --git a/src/main/java/com/example/ledgersystem/service/AccountServiceImpl.java b/src/main/java/com/example/ledgersystem/service/AccountServiceImpl.java index dbe3a99..585a7a2 100644 --- a/src/main/java/com/example/ledgersystem/service/AccountServiceImpl.java +++ b/src/main/java/com/example/ledgersystem/service/AccountServiceImpl.java @@ -61,35 +61,53 @@ public class AccountServiceImpl implements AccountService { @Transactional @Override public ApiResponse transfer(UUID fromAccountId, UUID toAccountId, BigDecimal amount, String refrenceId, UUID authenticatedUserId){ - + log.info("Transfer initiated: fromAccount={}, toAccount={}, amount={}, reference={}, user={}", + fromAccountId, toAccountId, amount, refrenceId, authenticatedUserId); + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + log.warn("Transfer rejected: amount must be greater than zero, amount={}, reference={}", amount, refrenceId); throw new APIexception("Transfer amount must be greater than zero"); } - + + log.debug("Checking for duplicate transaction: reference={}", refrenceId); Optional trans = transactionRepository.findByReferenceId(refrenceId); if(trans.isPresent()){ + log.warn("Duplicate transaction detected: reference={}, user={}", refrenceId, authenticatedUserId); throw new DuplicateTransactionException(refrenceId); } Optional existingSendersAccount = accountRepository.findById(fromAccountId); if(existingSendersAccount.isEmpty()){ + log.error("Sender account not found: fromAccount={}", fromAccountId); throw new AccountNotFound(fromAccountId); } boolean isCentralVault = existingSendersAccount.get().getName().equals("CENTRAL_BANK"); UUID ownerId = existingSendersAccount.get().getUser().getUser_id(); - + log.debug("Sender account found: name={}, balance={}, isCentralVault={}", + existingSendersAccount.get().getName(), + existingSendersAccount.get().getBalance(), + isCentralVault); + if (!isCentralVault && !ownerId.equals(authenticatedUserId)) { + log.warn("Unauthorized transfer attempt: fromAccount={}, requestedBy={}, owner={}", + fromAccountId, authenticatedUserId, ownerId); throw new APIexception("You do not own this account!"); } Optional existingRecieversAccount = accountRepository.findById(toAccountId); if(existingRecieversAccount.isEmpty()){ + log.error("Receiver account not found: toAccount={}", toAccountId); throw new AccountNotFound(toAccountId); } - + log.debug("Receiver account found: name={}, balance={}", + existingRecieversAccount.get().getName(), + existingRecieversAccount.get().getBalance()); + if(!existingSendersAccount.get().getName().equals("CENTRAL_BANK") && existingSendersAccount.get().getBalance().compareTo(amount) < 0){ + log.warn("Insufficient funds: requested={}, available={}, fromAccount={}, user={}", + amount, existingSendersAccount.get().getBalance(), fromAccountId, authenticatedUserId); throw new InsufficientFundsException(fromAccountId); } @@ -103,8 +121,10 @@ public ApiResponse transfer(UUID fromAccountId, UUID toAccountId, BigDecimal amo createLedgerEntry(existingSendersAccount.get(), amount.negate(), transaction); createLedgerEntry(existingRecieversAccount.get(), amount, transaction); - existingSendersAccount.get().setBalance(existingSendersAccount.get().getBalance().subtract(amount)); - existingRecieversAccount.get().setBalance(existingRecieversAccount.get().getBalance().add(amount)); + BigDecimal senderNewBalance = existingSendersAccount.get().getBalance().subtract(amount); + BigDecimal receiverNewBalance = existingRecieversAccount.get().getBalance().add(amount); + existingSendersAccount.get().setBalance(senderNewBalance); + existingRecieversAccount.get().setBalance(receiverNewBalance); accountRepository.save(existingSendersAccount.get()); accountRepository.save(existingRecieversAccount.get()); @@ -115,7 +135,11 @@ public ApiResponse transfer(UUID fromAccountId, UUID toAccountId, BigDecimal amo // 2. Invalidate Receiver String receiverKey = "balance:" + toAccountId; redisTemplate.delete(receiverKey); - + log.debug("Cache invalidated for accounts: fromAccount={}, toAccount={}", fromAccountId, toAccountId); + + log.info("Transfer completed: reference={}, amount={}, fromAccount={}, fromNewBalance={}, toAccount={}, toNewBalance={}", + refrenceId, amount, fromAccountId, senderNewBalance, toAccountId, receiverNewBalance); + String message = "Transfer successful"; ApiResponse response = new ApiResponse(); response.setMessage(message); @@ -151,7 +175,9 @@ private LedgerEntry createLedgerEntry(Account account, BigDecimal amount, Transa @Transactional @Override public ApiResponse deposit(UUID toAccountId, BigDecimal amount, String refId, UUID authenticatedUserId) { - + log.info("Deposit initiated: toAccount={}, amount={}, reference={}, user={}", + toAccountId, amount, refId, authenticatedUserId); + // 1. Fetch the Target Account Account targetAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new AccountNotFound(toAccountId)); @@ -159,15 +185,19 @@ public ApiResponse deposit(UUID toAccountId, BigDecimal amount, String refId, UU // 2. SECURITY CHECK: Ensure the User is depositing into THEIR OWN account UUID ownerId = targetAccount.getUser().getUser_id(); if (!ownerId.equals(authenticatedUserId)) { + log.warn("Unauthorized deposit attempt: toAccount={}, requestedBy={}, owner={}", + toAccountId, authenticatedUserId, ownerId); throw new APIexception("Security Alert: You can only deposit funds into your own account!"); } // 3. Fetch Central Bank Optional centralBank = accountRepository.findByName("CENTRAL_BANK"); if(centralBank.isEmpty()){ + log.error("System error: CENTRAL_BANK account not found during deposit, reference={}", refId); throw new APIexception("System Error: Central Bank Vault missing"); } + log.debug("Delegating deposit to transfer: centralBank={}, toAccount={}", centralBank.get().getAccountId(), toAccountId); // 4. Perform the Transfer (Using the Bypass Logic we added) // We pass "system" or specific logic to bypass the ownership check for the SENDER (Central Bank) // But we have already verified the RECEIVER above. @@ -176,6 +206,7 @@ public ApiResponse deposit(UUID toAccountId, BigDecimal amount, String refId, UU @Override public BigDecimal getBalance(UUID accountId, UUID authenticatedUserId) { + log.debug("Balance requested: accountId={}, user={}", accountId, authenticatedUserId); String key = "balance:" + accountId; // --- 1. FAST PATH (Redis) --- @@ -188,15 +219,16 @@ public BigDecimal getBalance(UUID accountId, UUID authenticatedUserId) { // 🔒 SECURITY CHECK (In Memory - 0ms) if (!cacheDto.getOwnerId().equals(authenticatedUserId)) { + log.warn("Unauthorized balance access from cache: accountId={}, requestedBy={}", accountId, authenticatedUserId); throw new APIexception("Security Alert: You do not own this account!"); } - System.out.println("🚀 CACHE HIT: Returning securely from Redis!"); + log.debug("Cache hit: returning balance from Redis for accountId={}", accountId); return cacheDto.getBalance(); } // --- 2. SLOW PATH (Database) --- - System.out.println("🐢 CACHE MISS: Fetching from DB..."); + log.debug("Cache miss: fetching balance from DB for accountId={}", accountId); }catch (RedisConnectionFailureException e){ log.warn("Redis unavailable, continuing without cache: {}", e.getMessage()); } @@ -206,6 +238,7 @@ public BigDecimal getBalance(UUID accountId, UUID authenticatedUserId) { // 🔒 SECURITY CHECK (Database Level) if (!account.getUser().getUser_id().equals(authenticatedUserId)) { + log.warn("Unauthorized balance access from DB: accountId={}, requestedBy={}", accountId, authenticatedUserId); throw new APIexception("Security Alert: You do not own this account!"); } @@ -220,22 +253,28 @@ public BigDecimal getBalance(UUID accountId, UUID authenticatedUserId) { try { // Save to Redis (Expires in 10 mins) redisTemplate.opsForValue().set(key, newCacheEntry, 10, TimeUnit.MINUTES); + log.debug("Balance cached in Redis: accountId={}, ttl=10min", accountId); }catch (RedisConnectionFailureException e){ log.warn("Redis unavailable, continuing without cache: {}", e.getMessage()); } + log.info("Balance retrieved from DB: accountId={}, balance={}, user={}", accountId, balance, authenticatedUserId); return balance; } @Override public StatementResponse accountStatement(UUID accountId, Integer pageNumber, Integer pageSize, String sortBy, String sortOrder, UUID authenticatedUserId){ + log.info("Account statement requested: accountId={}, pageNumber={}, pageSize={}, sortBy={}, sortOrder={}, user={}", + accountId, pageNumber, pageSize, sortBy, sortOrder, authenticatedUserId); Optional existingAccount = accountRepository.findById(accountId); if(existingAccount.isEmpty()){ + log.error("Account not found for statement: accountId={}", accountId); throw new AccountNotFound(accountId); } UUID ownerId = existingAccount.get().getUser().getUser_id(); if (!ownerId.equals(authenticatedUserId)) { + log.warn("Unauthorized statement access: accountId={}, requestedBy={}, owner={}", accountId, authenticatedUserId, ownerId); throw new APIexception("You do not own this account!"); } @@ -264,6 +303,8 @@ public StatementResponse accountStatement(UUID accountId, Integer pageNumber, In statementResponse.setTotalElements(pagedLedgers.getTotalElements()); statementResponse.setTotalPages(pagedLedgers.getTotalPages()); + log.debug("Account statement returned: accountId={}, entries={}, totalPages={}, user={}", + accountId, entries.size(), pagedLedgers.getTotalPages(), authenticatedUserId); return statementResponse; } } diff --git a/src/main/java/com/example/ledgersystem/service/AdminServiceImpl.java b/src/main/java/com/example/ledgersystem/service/AdminServiceImpl.java index 76d4b96..3765586 100644 --- a/src/main/java/com/example/ledgersystem/service/AdminServiceImpl.java +++ b/src/main/java/com/example/ledgersystem/service/AdminServiceImpl.java @@ -4,20 +4,25 @@ import com.example.ledgersystem.model.LedgerEntry; import com.example.ledgersystem.repositories.LedgerEntryRepository; import com.example.ledgersystem.utils.HashUtils; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.UUID; + @Service +@Slf4j public class AdminServiceImpl implements AdminService { @Autowired private LedgerEntryRepository ledgerEntryRepository; @Override public Auditresponse audit(UUID accountId) { + log.info("Audit started: accountId={}", accountId); // 1. CRITICAL: Fetch Oldest-to-Newest (ASC) to replay history List ledgerEntries = ledgerEntryRepository.findLedgerEntryByAccount_AccountIdOrderByLoggedAtAsc(accountId); + log.debug("Audit: fetched {} ledger entries for accountId={}", ledgerEntries.size(), accountId); // 2. Start with the Genesis Hash (Same constant you used in Service) String lastSeenHash = "MANK_1008"; @@ -27,6 +32,8 @@ public Auditresponse audit(UUID accountId) { // --- CHECK 1: The Link (Chain Integrity) --- // Does this row point to the correct previous row? if (!ledgerEntry.getPrevHash().equals(lastSeenHash)) { + log.warn("Audit failed - broken chain: accountId={}, ledgerId={}, expectedPrevHash={}, actualPrevHash={}", + accountId, ledgerEntry.getLedgerId(), lastSeenHash, ledgerEntry.getPrevHash()); return new Auditresponse("CORRUPTED: BROKEN CHAIN" , ledgerEntry.getLedgerId()); } @@ -37,11 +44,12 @@ public Auditresponse audit(UUID accountId) { amountString + ledgerEntry.getTransaction().getReferenceId() + ledgerEntry.getLoggedAt().toString(); - System.out.println("AUDIT GENERATED: " + dataContent); + log.debug("Audit checking entry: ledgerId={}, reference={}", ledgerEntry.getLedgerId(), ledgerEntry.getTransaction().getReferenceId()); String calculatedHash = HashUtils.generateHash(dataContent); - System.out.println("AUDIT: " + calculatedHash); if (!calculatedHash.equals(ledgerEntry.getHash())) { + log.warn("Audit failed - data modified: accountId={}, ledgerId={}, reference={}", + accountId, ledgerEntry.getLedgerId(), ledgerEntry.getTransaction().getReferenceId()); return new Auditresponse("CORRUPTED: DATA MODIFIED" , ledgerEntry.getLedgerId()); } @@ -50,6 +58,7 @@ public Auditresponse audit(UUID accountId) { lastSeenHash = ledgerEntry.getHash(); } + log.info("Audit completed: accountId={}, result=VALID, entriesChecked={}", accountId, ledgerEntries.size()); return new Auditresponse("VALID", null); } } diff --git a/src/main/java/com/example/ledgersystem/service/RateLimitingService.java b/src/main/java/com/example/ledgersystem/service/RateLimitingService.java index a7ca438..7840514 100644 --- a/src/main/java/com/example/ledgersystem/service/RateLimitingService.java +++ b/src/main/java/com/example/ledgersystem/service/RateLimitingService.java @@ -6,6 +6,7 @@ import io.github.bucket4j.BucketConfiguration; import io.github.bucket4j.Refill; import io.github.bucket4j.distributed.proxy.ProxyManager; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,6 +15,7 @@ import java.util.function.Supplier; @Service +@Slf4j public class RateLimitingService { @Autowired @@ -22,11 +24,13 @@ public class RateLimitingService { public Bucket resolveBucket(UUID userId, RateLimitType type) { // 1. DIFFERENT KEYS: "rate_limit:GENERAL:uuid" vs "rate_limit:TRANSACTION:uuid" String key = "rate_limit:" + type.name() + ":" + userId.toString(); + log.debug("Resolving rate limit bucket: userId={}, type={}, key={}", userId, type, key); return proxyManager.builder().build(key, () -> getConfig(type)); } private BucketConfiguration getConfig(RateLimitType type) { + log.debug("Creating new rate limit bucket config: type={}", type); if (type == RateLimitType.TRANSACTION) { // 🛡️ STRICT: 1 request per minute (No bursts) return BucketConfiguration.builder() diff --git a/src/main/java/com/example/ledgersystem/utils/AuthUtils.java b/src/main/java/com/example/ledgersystem/utils/AuthUtils.java index c75d49f..990f96e 100644 --- a/src/main/java/com/example/ledgersystem/utils/AuthUtils.java +++ b/src/main/java/com/example/ledgersystem/utils/AuthUtils.java @@ -3,6 +3,7 @@ import com.example.ledgersystem.Security.Services.UserDetailsImpl; import com.example.ledgersystem.model.User; import com.example.ledgersystem.repositories.UserRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -12,12 +13,14 @@ import java.util.UUID; @Component +@Slf4j public class AuthUtils { @Autowired UserRepository userRepository; public String loggedInEmail(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + log.debug("Fetching email for user: {}", authentication.getName()); User user = userRepository.findByUsername(authentication.getName()) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + authentication.getName())); @@ -30,12 +33,14 @@ public UUID loggedInUserId(){ // Cast the "Principal" to your custom class UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + log.debug("Fetching userId from security context: userId={}", userDetails.getId()); // Return the ID directly from memory (0ms latency) return userDetails.getId(); } public User loggedInUser(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + log.debug("Fetching full user object for: {}", authentication.getName()); User user = userRepository.findByUsername(authentication.getName()) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + authentication.getName())); diff --git a/src/main/java/com/example/ledgersystem/utils/HashUtils.java b/src/main/java/com/example/ledgersystem/utils/HashUtils.java index 435c436..7fdfd0d 100644 --- a/src/main/java/com/example/ledgersystem/utils/HashUtils.java +++ b/src/main/java/com/example/ledgersystem/utils/HashUtils.java @@ -1,17 +1,24 @@ package com.example.ledgersystem.utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashUtils { - + + private static final Logger log = LoggerFactory.getLogger(HashUtils.class); + public static String generateHash(String input) { + log.debug("Generating SHA-256 hash for input of length={}", input.length()); try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] encodedhash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); return bytesToHex(encodedhash); } catch (NoSuchAlgorithmException e) { + log.error("SHA-256 algorithm not found", e); throw new RuntimeException("SHA-256 algorithm not found", e); } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..aac3af5 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,48 @@ + + + + + + + + + + ${LOG_PATTERN} + + + + + + ${LOG_PATH}/ledgersystem.log + + + ${LOG_PATH}/ledgersystem.%d{yyyy-MM-dd}.%i.log.gz + + 100MB + + 30 + + + ${LOG_PATTERN} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..0650941 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ + + + + + + + + ${LOG_PATTERN} + + + + + + + + + + + + +