feat(security): implement persistent account suspension on brute force, login lockout, and security patches

This commit is contained in:
Shanmuga Krishnan S M
2026-06-23 08:32:00 +05:30
parent 9c7ba0f0bd
commit 39249ab34f
10 changed files with 280 additions and 30 deletions

View File

@@ -81,6 +81,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated() .requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated() .requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated() .requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
// ── STAFF/MANAGER/MASTER: All other management APIs ── // ── STAFF/MANAGER/MASTER: All other management APIs ──
.requestMatchers("/api/**").hasAnyRole("MASTER", "MANAGER", "STAFF", "OPERATOR") .requestMatchers("/api/**").hasAnyRole("MASTER", "MANAGER", "STAFF", "OPERATOR")
@@ -106,9 +107,7 @@ public class SecurityConfig {
List<String> origins = Arrays.asList(allowedOriginsStr.split(",")); List<String> origins = Arrays.asList(allowedOriginsStr.split(","));
configuration.setAllowedOrigins(origins); configuration.setAllowedOrigins(origins);
configuration.setAllowedOriginPatterns(List.of( configuration.setAllowedOriginPatterns(List.of(
"http://localhost:*", "http://localhost:*"
"http://192.168.*:*",
"http://10.*:*"
)); ));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));

View File

@@ -9,6 +9,9 @@ import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import io.jsonwebtoken.Claims;
@RestController @RestController
@RequestMapping("/api/coupons") @RequestMapping("/api/coupons")
@@ -38,6 +41,10 @@ public class CouponController {
String code = ((String) request.get("code")).toUpperCase().trim(); String code = ((String) request.get("code")).toUpperCase().trim();
Long userId = Long.valueOf(request.get("userId").toString()); Long userId = Long.valueOf(request.get("userId").toString());
if (!canAccessUser(userId)) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "Access denied"));
}
Optional<CouponCode> couponOpt = couponRepository.findByCode(code); Optional<CouponCode> couponOpt = couponRepository.findByCode(code);
if (couponOpt.isEmpty()) { if (couponOpt.isEmpty()) {
return ResponseEntity.status(404).body(Map.of("success", false, "message", "Invalid coupon code")); return ResponseEntity.status(404).body(Map.of("success", false, "message", "Invalid coupon code"));
@@ -112,4 +119,22 @@ public class CouponController {
} }
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
private boolean canAccessUser(Long targetUserId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) return false;
if (auth.getDetails() instanceof Claims claims) {
String role = (String) claims.get("role");
// Staff roles can access anyone
if ("MASTER".equals(role) || "MANAGER".equals(role) || "STAFF".equals(role)) return true;
// Customers can only access themselves
Object uid = claims.get("userId");
if (uid != null) {
Long tokenUserId = uid instanceof Integer ? ((Integer) uid).longValue() : (Long) uid;
return tokenUserId.equals(targetUserId);
}
}
return false;
}
} }

View File

@@ -14,6 +14,9 @@ import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.rit.canteen.sales.service.SystemNotificationService; import com.rit.canteen.sales.service.SystemNotificationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import io.jsonwebtoken.Claims;
import java.util.*; import java.util.*;
@@ -134,6 +137,9 @@ public class FeedbackController {
@GetMapping("/latest-unrated/{userId}") @GetMapping("/latest-unrated/{userId}")
public ResponseEntity<Order> getLatestUnratedOrder(@PathVariable Long userId) { public ResponseEntity<Order> getLatestUnratedOrder(@PathVariable Long userId) {
if (!canAccessUser(userId)) {
return ResponseEntity.status(403).build();
}
// Look for any order that is either PAID or COMPLETED // Look for any order that is either PAID or COMPLETED
Optional<Order> latestOrderOpt = orderRepository.findFirstByUserIdOrderByCreatedAtDesc(userId); Optional<Order> latestOrderOpt = orderRepository.findFirstByUserIdOrderByCreatedAtDesc(userId);
@@ -153,6 +159,9 @@ public class FeedbackController {
Optional<Order> orderOpt = orderRepository.findById(orderId); Optional<Order> orderOpt = orderRepository.findById(orderId);
if (orderOpt.isPresent()) { if (orderOpt.isPresent()) {
Order order = orderOpt.get(); Order order = orderOpt.get();
if (!canAccessUser(order.getUserId())) {
return ResponseEntity.status(403).build();
}
order.setHasFeedback(true); order.setHasFeedback(true);
orderRepository.save(order); orderRepository.save(order);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
@@ -173,6 +182,9 @@ public class FeedbackController {
} }
Order order = orderOpt.get(); Order order = orderOpt.get();
if (!canAccessUser(order.getUserId())) {
return ResponseEntity.status(403).build();
}
feedback.setOrder(order); feedback.setOrder(order);
// Link item ratings to feedback and propagate comment // Link item ratings to feedback and propagate comment
@@ -202,4 +214,22 @@ public class FeedbackController {
return ResponseEntity.ok(savedFeedback); return ResponseEntity.ok(savedFeedback);
} }
private boolean canAccessUser(Long targetUserId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) return false;
if (auth.getDetails() instanceof Claims claims) {
String role = (String) claims.get("role");
// Staff roles can access anyone
if ("MASTER".equals(role) || "MANAGER".equals(role) || "STAFF".equals(role)) return true;
// Customers can only access themselves
Object uid = claims.get("userId");
if (uid != null) {
Long tokenUserId = uid instanceof Integer ? ((Integer) uid).longValue() : (Long) uid;
return tokenUserId.equals(targetUserId);
}
}
return false;
}
} }

View File

@@ -7,10 +7,12 @@ import com.rit.canteen.sales.service.SystemUserService;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.rit.canteen.sales.service.LoginLockoutService;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -30,6 +32,12 @@ public class SystemAuthController {
@Autowired @Autowired
private LoginRateLimiter rateLimiter; private LoginRateLimiter rateLimiter;
@Autowired
private LoginLockoutService lockoutService;
@Value("${app.security.trust-proxy-headers:false}")
private boolean trustProxyHeaders;
// ── PUBLIC ────────────────────────────────────────────────────────────── // ── PUBLIC ──────────────────────────────────────────────────────────────
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> credentials, public ResponseEntity<?> login(@RequestBody Map<String, String> credentials,
@@ -45,9 +53,22 @@ public class SystemAuthController {
String email = credentials.get("email"); String email = credentials.get("email");
String password = credentials.get("password"); String password = credentials.get("password");
if (email != null && lockoutService.isLockedOut(email)) {
long remainingMinutes = (lockoutService.getRemainingLockoutTimeMs(email) / 1000) / 60;
if (remainingMinutes == 0) {
remainingMinutes = 1;
}
return ResponseEntity.status(423).body(Map.of(
"error", "Account is locked due to too many failed attempts. Please try again in " + remainingMinutes + " minutes."
));
}
Optional<SystemUser> userOpt = userService.authenticate(email, password); Optional<SystemUser> userOpt = userService.authenticate(email, password);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
if (email != null) {
lockoutService.resetAttempts(email);
}
SystemUser user = userOpt.get(); SystemUser user = userOpt.get();
String token = jwtUtil.generateToken(user.getId(), user.getEmail(), String token = jwtUtil.generateToken(user.getId(), user.getEmail(),
user.getRole(), user.getPermissions()); user.getRole(), user.getPermissions());
@@ -62,6 +83,9 @@ public class SystemAuthController {
response.put("viewOnly", user.isViewOnly()); response.put("viewOnly", user.isViewOnly());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} else { } else {
if (email != null) {
lockoutService.registerFailedAttempt(email);
}
return ResponseEntity.status(401).body(Map.of("error", "Invalid credentials")); return ResponseEntity.status(401).body(Map.of("error", "Invalid credentials"));
} }
} }
@@ -164,10 +188,12 @@ public class SystemAuthController {
} }
private String getClientIp(HttpServletRequest request) { private String getClientIp(HttpServletRequest request) {
if (trustProxyHeaders) {
String xfHeader = request.getHeader("X-Forwarded-For"); String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader != null && !xfHeader.isEmpty()) { if (xfHeader != null && !xfHeader.isEmpty()) {
return xfHeader.split(",")[0].trim(); return xfHeader.split(",")[0].trim();
} }
}
return request.getRemoteAddr(); return request.getRemoteAddr();
} }
} }

View File

@@ -13,6 +13,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import com.rit.canteen.sales.service.LoginLockoutService;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import java.util.HashMap; import java.util.HashMap;
@@ -32,6 +34,12 @@ public class UserController {
@Autowired @Autowired
private LoginRateLimiter rateLimiter; private LoginRateLimiter rateLimiter;
@Autowired
private LoginLockoutService lockoutService;
@Value("${app.security.trust-proxy-headers:false}")
private boolean trustProxyHeaders;
// ── PUBLIC ──────────────────────────────────────────────────────────────── // ── PUBLIC ────────────────────────────────────────────────────────────────
@PostMapping("/check") @PostMapping("/check")
@@ -69,17 +77,54 @@ public class UserController {
LoginResponse rateResp = new LoginResponse(false, "Too many login attempts. Please wait 5 minutes."); LoginResponse rateResp = new LoginResponse(false, "Too many login attempts. Please wait 5 minutes.");
return ResponseEntity.status(429).body(rateResp); return ResponseEntity.status(429).body(rateResp);
} }
LoginResponse response = userService.verifyPinAndLogin(request.getMobileNumber(), request.getPin());
String mobileNumber = request.getMobileNumber();
// 1. Check if user is suspended in DB first, because suspension takes precedence over in-memory lockout
if (mobileNumber != null) {
LoginResponse.UserDto userDto = userService.getUserByMobile(mobileNumber);
if (userDto != null && userDto.isSuspended()) {
LoginResponse suspResp = new LoginResponse(false, "Your account has been suspended. Please contact the administrator.", userDto);
return ResponseEntity.status(403).body(suspResp);
}
}
// 2. Check in-memory lockout
if (mobileNumber != null && lockoutService.isLockedOut(mobileNumber)) {
long remainingMinutes = (lockoutService.getRemainingLockoutTimeMs(mobileNumber) / 1000) / 60;
if (remainingMinutes == 0) {
remainingMinutes = 1;
}
LoginResponse lockResp = new LoginResponse(false,
"Account is locked due to too many failed attempts. Please try again in " + remainingMinutes + " minutes.");
return ResponseEntity.status(423).body(lockResp);
}
LoginResponse response = userService.verifyPinAndLogin(mobileNumber, request.getPin());
if (response.isSuccess()) { if (response.isSuccess()) {
if (mobileNumber != null) {
lockoutService.resetAttempts(mobileNumber);
}
Long userId = response.getUser() != null ? response.getUser().getId() : null; Long userId = response.getUser() != null ? response.getUser().getId() : null;
if (userId != null) { if (userId != null) {
String token = jwtUtil.generateUserToken(userId, request.getMobileNumber()); String token = jwtUtil.generateUserToken(userId, mobileNumber);
response.setToken(token); response.setToken(token);
} }
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} else {
if (mobileNumber != null) {
lockoutService.registerFailedAttempt(mobileNumber);
if (lockoutService.isLockedOut(mobileNumber)) {
userService.suspendUserByMobile(mobileNumber);
LoginResponse.UserDto userDto = userService.getUserByMobile(mobileNumber);
LoginResponse lockResp = new LoginResponse(false,
"Too many login attempts. Your account has been suspended indefinitely due to brute force detection. Please contact the administrator.", userDto);
return ResponseEntity.status(423).body(lockResp);
}
} }
return ResponseEntity.badRequest().body(response); return ResponseEntity.badRequest().body(response);
} }
}
@PostMapping("/logout") @PostMapping("/logout")
public ResponseEntity<LoginResponse> logout(@Valid @RequestBody LoginRequest request) { public ResponseEntity<LoginResponse> logout(@Valid @RequestBody LoginRequest request) {
@@ -147,6 +192,10 @@ public class UserController {
@PatchMapping("/users/{id}/suspend") @PatchMapping("/users/{id}/suspend")
public ResponseEntity<LoginResponse.UserDto> toggleSuspension(@PathVariable Long id) { public ResponseEntity<LoginResponse.UserDto> toggleSuspension(@PathVariable Long id) {
LoginResponse.UserDto updated = userService.toggleSuspension(id); LoginResponse.UserDto updated = userService.toggleSuspension(id);
if (updated != null && !updated.isSuspended()) {
// Reset in-memory lockout attempts if the account was unsuspended
lockoutService.resetAttempts(updated.getMobileNumber());
}
return updated != null ? ResponseEntity.ok(updated) : ResponseEntity.notFound().build(); return updated != null ? ResponseEntity.ok(updated) : ResponseEntity.notFound().build();
} }
@@ -185,8 +234,10 @@ public class UserController {
} }
private String getClientIp(HttpServletRequest request) { private String getClientIp(HttpServletRequest request) {
if (trustProxyHeaders) {
String xfHeader = request.getHeader("X-Forwarded-For"); String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader != null && !xfHeader.isEmpty()) return xfHeader.split(",")[0].trim(); if (xfHeader != null && !xfHeader.isEmpty()) return xfHeader.split(",")[0].trim();
}
return request.getRemoteAddr(); return request.getRemoteAddr();
} }
} }

View File

@@ -24,17 +24,35 @@ public class CounterImageUploadController {
return ResponseEntity.badRequest().body(Map.of("error", "No file selected")); return ResponseEntity.badRequest().body(Map.of("error", "No file selected"));
} }
// 1. File size check (e.g. 5MB limit)
long maxSizeBytes = 5 * 1024 * 1024;
if (file.getSize() > maxSizeBytes) {
return ResponseEntity.badRequest().body(Map.of("error", "File exceeds maximum limit of 5MB"));
}
// 2. MIME type check
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return ResponseEntity.badRequest().body(Map.of("error", "Only image files are allowed"));
}
// 3. Extension check
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !originalFilename.contains(".")) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid file name. Extension required."));
}
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
if (!extension.equals(".jpg") && !extension.equals(".jpeg") && !extension.equals(".png") && !extension.equals(".gif") && !extension.equals(".webp")) {
return ResponseEntity.badRequest().body(Map.of("error", "Only JPG, JPEG, PNG, GIF, and WEBP images are allowed"));
}
try { try {
// Create uploads directory if it doesn't exist // Create uploads directory if it doesn't exist
Path uploadPath = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize(); Path uploadPath = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize();
Files.createDirectories(uploadPath); Files.createDirectories(uploadPath);
// Generate unique filename to prevent collisions // Generate unique filename to prevent collisions
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String uniqueFilename = UUID.randomUUID().toString() + extension; String uniqueFilename = UUID.randomUUID().toString() + extension;
// Save the file // Save the file

View File

@@ -0,0 +1,91 @@
package com.rit.canteen.sales.service;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service to manage account lockouts due to consecutive failed login attempts.
* If 5 failed attempts occur where successive attempts are within 5 seconds,
* or 5 failed attempts occur in a 5-second window, the account is locked for 1 hour.
*/
@Service
public class LoginLockoutService {
private static final long LOCKOUT_DURATION_MS = 60 * 60 * 1000L; // 1 hour
private static final int MAX_ATTEMPTS = 5;
private static final long MAX_INTERVAL_MS = 5000L; // 5 seconds
private final Map<String, Long> lockouts = new ConcurrentHashMap<>();
private final Map<String, List<Long>> failedAttempts = new ConcurrentHashMap<>();
public boolean isLockedOut(String key) {
if (key == null) return false;
Long lockoutExpiry = lockouts.get(key);
if (lockoutExpiry != null) {
if (Instant.now().toEpochMilli() < lockoutExpiry) {
return true;
} else {
// Lockout expired - clean up
lockouts.remove(key);
failedAttempts.remove(key);
}
}
return false;
}
public long getRemainingLockoutTimeMs(String key) {
if (key == null) return 0;
Long lockoutExpiry = lockouts.get(key);
if (lockoutExpiry != null) {
long remaining = lockoutExpiry - Instant.now().toEpochMilli();
return Math.max(0, remaining);
}
return 0;
}
public void registerFailedAttempt(String key) {
if (key == null) return;
long now = Instant.now().toEpochMilli();
List<Long> attempts = failedAttempts.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<>()));
synchronized (attempts) {
attempts.add(now);
// Keep only the last MAX_ATTEMPTS attempts
while (attempts.size() > MAX_ATTEMPTS) {
attempts.remove(0);
}
if (attempts.size() == MAX_ATTEMPTS) {
// Check if all 5 attempts occurred within a 5 second window
long firstAttempt = attempts.get(0);
long lastAttempt = attempts.get(attempts.size() - 1);
boolean allWithinWindow = (lastAttempt - firstAttempt) <= MAX_INTERVAL_MS;
// Also check if each successive attempt is within 5 seconds of the previous one
boolean successiveWithinInterval = true;
for (int i = 1; i < attempts.size(); i++) {
if ((attempts.get(i) - attempts.get(i - 1)) > MAX_INTERVAL_MS) {
successiveWithinInterval = false;
break;
}
}
if (allWithinWindow || successiveWithinInterval) {
lockouts.put(key, now + LOCKOUT_DURATION_MS);
}
}
}
}
public void resetAttempts(String key) {
if (key == null) return;
failedAttempts.remove(key);
lockouts.remove(key);
}
}

View File

@@ -109,25 +109,18 @@ public class SystemUserService {
} }
public Optional<SystemUser> authenticate(String email, String password) { public Optional<SystemUser> authenticate(String email, String password) {
System.out.println(">>> Attempting authentication for: " + email);
// 1. Try Database First // 1. Try Database First
Optional<SystemUser> user = repository.findByEmail(email); Optional<SystemUser> user = repository.findByEmail(email);
if (user.isPresent()) { if (user.isPresent()) {
boolean matches = passwordEncoder.matches(password, user.get().getPassword()); boolean matches = passwordEncoder.matches(password, user.get().getPassword());
System.out.println(">>> User found in DB. Password match: " + matches);
if (matches) { if (matches) {
return user; return user;
} }
} else { } else {
System.out.println(">>> User NOT found in DB. Checking Failsafe eligibility...");
// 2. Try Failsafe (Properties) - ONLY if no Master users exist in DB // 2. Try Failsafe (Properties) - ONLY if no Master users exist in DB
List<SystemUser> masters = repository.findByRole("MASTER"); List<SystemUser> masters = repository.findByRole("MASTER");
if (masters.isEmpty()) { if (masters.isEmpty()) {
if (email.equals(masterUsername) && password.equals(masterPassword)) { if (email.equals(masterUsername) && passwordEncoder.matches(password, passwordEncoder.encode(masterPassword))) {
System.out.println(">>> FAILSAFE AUTHENTICATION SUCCESSFUL (No DB Master Found)");
SystemUser failsafeUser = new SystemUser(); SystemUser failsafeUser = new SystemUser();
failsafeUser.setId(0L); failsafeUser.setId(0L);
failsafeUser.setName("Failsafe Admin"); failsafeUser.setName("Failsafe Admin");
@@ -136,8 +129,6 @@ public class SystemUserService {
failsafeUser.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); failsafeUser.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
return Optional.of(failsafeUser); return Optional.of(failsafeUser);
} }
} else {
System.out.println(">>> Failsafe disabled because custom master account exists in database.");
} }
} }

View File

@@ -14,6 +14,7 @@ import java.math.BigDecimal;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -132,10 +133,10 @@ public class TokenService {
// PERMANENT REMOVAL: Physical delete from database // PERMANENT REMOVAL: Physical delete from database
List<Long> unitIds = unitsToSpend.stream().map(TokenUnit::getId).collect(Collectors.toList()); List<Long> unitIds = unitsToSpend.stream().map(TokenUnit::getId).collect(Collectors.toList());
String deleteSql = "DELETE FROM token_units WHERE id IN (" + String placeholders = Collections.nCopies(unitIds.size(), "?").stream().collect(Collectors.joining(","));
unitIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")"; String deleteSql = "DELETE FROM token_units WHERE id IN (" + placeholders + ")";
jdbcTemplate.update(deleteSql); jdbcTemplate.update(deleteSql, unitIds.toArray());
// Update cached user balance // Update cached user balance
user.setRitzTokenBalance(currentBalance.subtract(amount)); user.setRitzTokenBalance(currentBalance.subtract(amount));

View File

@@ -80,7 +80,10 @@ public class UserService {
User user = userOpt.get(); User user = userOpt.get();
if (user.isSuspended()) { if (user.isSuspended()) {
return new LoginResponse(false, "Your account has been suspended. Please contact the administrator."); LoginResponse.UserDto userDto = new LoginResponse.UserDto(
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()
);
return new LoginResponse(false, "Your account has been suspended. Please contact the administrator.", userDto);
} }
if (!passwordEncoder.matches(pin, user.getPinHash())) { if (!passwordEncoder.matches(pin, user.getPinHash())) {
@@ -241,6 +244,20 @@ public class UserService {
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()); return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance());
} }
/**
* Suspend a user by their mobile number.
*/
public void suspendUserByMobile(String mobileNumber) {
if (mobileNumber == null) return;
userRepository.findByMobileNumber(mobileNumber).ifPresent(user -> {
if (!user.isSuspended()) {
user.setSuspended(true);
user.setLoggedIn(false);
userRepository.save(user);
}
});
}
/** /**
* Delete a user from the system. * Delete a user from the system.
*/ */
@@ -248,3 +265,4 @@ public class UserService {
userRepository.deleteById(userId); userRepository.deleteById(userId);
} }
} }