feat(security): implement persistent account suspension on brute force, login lockout, and security patches
This commit is contained in:
@@ -81,6 +81,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
|
||||
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
|
||||
|
||||
// ── STAFF/MANAGER/MASTER: All other management APIs ──
|
||||
.requestMatchers("/api/**").hasAnyRole("MASTER", "MANAGER", "STAFF", "OPERATOR")
|
||||
@@ -106,9 +107,7 @@ public class SecurityConfig {
|
||||
List<String> origins = Arrays.asList(allowedOriginsStr.split(","));
|
||||
configuration.setAllowedOrigins(origins);
|
||||
configuration.setAllowedOriginPatterns(List.of(
|
||||
"http://localhost:*",
|
||||
"http://192.168.*:*",
|
||||
"http://10.*:*"
|
||||
"http://localhost:*"
|
||||
));
|
||||
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
|
||||
@@ -9,6 +9,9 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import io.jsonwebtoken.Claims;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/coupons")
|
||||
@@ -38,6 +41,10 @@ public class CouponController {
|
||||
String code = ((String) request.get("code")).toUpperCase().trim();
|
||||
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);
|
||||
if (couponOpt.isEmpty()) {
|
||||
return ResponseEntity.status(404).body(Map.of("success", false, "message", "Invalid coupon code"));
|
||||
@@ -112,4 +119,22 @@ public class CouponController {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
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.*;
|
||||
|
||||
@@ -134,6 +137,9 @@ public class FeedbackController {
|
||||
|
||||
@GetMapping("/latest-unrated/{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
|
||||
Optional<Order> latestOrderOpt = orderRepository.findFirstByUserIdOrderByCreatedAtDesc(userId);
|
||||
|
||||
@@ -153,6 +159,9 @@ public class FeedbackController {
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (orderOpt.isPresent()) {
|
||||
Order order = orderOpt.get();
|
||||
if (!canAccessUser(order.getUserId())) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
order.setHasFeedback(true);
|
||||
orderRepository.save(order);
|
||||
return ResponseEntity.ok().build();
|
||||
@@ -173,6 +182,9 @@ public class FeedbackController {
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
if (!canAccessUser(order.getUserId())) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
feedback.setOrder(order);
|
||||
|
||||
// Link item ratings to feedback and propagate comment
|
||||
@@ -202,4 +214,22 @@ public class FeedbackController {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import com.rit.canteen.sales.service.SystemUserService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.rit.canteen.sales.service.LoginLockoutService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -30,6 +32,12 @@ public class SystemAuthController {
|
||||
@Autowired
|
||||
private LoginRateLimiter rateLimiter;
|
||||
|
||||
@Autowired
|
||||
private LoginLockoutService lockoutService;
|
||||
|
||||
@Value("${app.security.trust-proxy-headers:false}")
|
||||
private boolean trustProxyHeaders;
|
||||
|
||||
// ── PUBLIC ──────────────────────────────────────────────────────────────
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody Map<String, String> credentials,
|
||||
@@ -45,9 +53,22 @@ public class SystemAuthController {
|
||||
String email = credentials.get("email");
|
||||
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);
|
||||
|
||||
if (userOpt.isPresent()) {
|
||||
if (email != null) {
|
||||
lockoutService.resetAttempts(email);
|
||||
}
|
||||
SystemUser user = userOpt.get();
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getEmail(),
|
||||
user.getRole(), user.getPermissions());
|
||||
@@ -62,6 +83,9 @@ public class SystemAuthController {
|
||||
response.put("viewOnly", user.isViewOnly());
|
||||
return ResponseEntity.ok(response);
|
||||
} else {
|
||||
if (email != null) {
|
||||
lockoutService.registerFailedAttempt(email);
|
||||
}
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid credentials"));
|
||||
}
|
||||
}
|
||||
@@ -164,9 +188,11 @@ public class SystemAuthController {
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String xfHeader = request.getHeader("X-Forwarded-For");
|
||||
if (xfHeader != null && !xfHeader.isEmpty()) {
|
||||
return xfHeader.split(",")[0].trim();
|
||||
if (trustProxyHeaders) {
|
||||
String xfHeader = request.getHeader("X-Forwarded-For");
|
||||
if (xfHeader != null && !xfHeader.isEmpty()) {
|
||||
return xfHeader.split(",")[0].trim();
|
||||
}
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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 java.util.HashMap;
|
||||
@@ -32,6 +34,12 @@ public class UserController {
|
||||
@Autowired
|
||||
private LoginRateLimiter rateLimiter;
|
||||
|
||||
@Autowired
|
||||
private LoginLockoutService lockoutService;
|
||||
|
||||
@Value("${app.security.trust-proxy-headers:false}")
|
||||
private boolean trustProxyHeaders;
|
||||
|
||||
// ── PUBLIC ────────────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/check")
|
||||
@@ -69,16 +77,53 @@ public class UserController {
|
||||
LoginResponse rateResp = new LoginResponse(false, "Too many login attempts. Please wait 5 minutes.");
|
||||
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 (mobileNumber != null) {
|
||||
lockoutService.resetAttempts(mobileNumber);
|
||||
}
|
||||
Long userId = response.getUser() != null ? response.getUser().getId() : null;
|
||||
if (userId != null) {
|
||||
String token = jwtUtil.generateUserToken(userId, request.getMobileNumber());
|
||||
String token = jwtUtil.generateUserToken(userId, mobileNumber);
|
||||
response.setToken(token);
|
||||
}
|
||||
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")
|
||||
@@ -147,6 +192,10 @@ public class UserController {
|
||||
@PatchMapping("/users/{id}/suspend")
|
||||
public ResponseEntity<LoginResponse.UserDto> toggleSuspension(@PathVariable Long 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();
|
||||
}
|
||||
|
||||
@@ -185,8 +234,10 @@ public class UserController {
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String xfHeader = request.getHeader("X-Forwarded-For");
|
||||
if (xfHeader != null && !xfHeader.isEmpty()) return xfHeader.split(",")[0].trim();
|
||||
if (trustProxyHeaders) {
|
||||
String xfHeader = request.getHeader("X-Forwarded-For");
|
||||
if (xfHeader != null && !xfHeader.isEmpty()) return xfHeader.split(",")[0].trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,35 @@ public class CounterImageUploadController {
|
||||
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 {
|
||||
// Create uploads directory if it doesn't exist
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize();
|
||||
Files.createDirectories(uploadPath);
|
||||
|
||||
// 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;
|
||||
|
||||
// Save the 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);
|
||||
}
|
||||
}
|
||||
@@ -109,25 +109,18 @@ public class SystemUserService {
|
||||
}
|
||||
|
||||
public Optional<SystemUser> authenticate(String email, String password) {
|
||||
System.out.println(">>> Attempting authentication for: " + email);
|
||||
|
||||
// 1. Try Database First
|
||||
Optional<SystemUser> user = repository.findByEmail(email);
|
||||
if (user.isPresent()) {
|
||||
boolean matches = passwordEncoder.matches(password, user.get().getPassword());
|
||||
System.out.println(">>> User found in DB. Password match: " + matches);
|
||||
if (matches) {
|
||||
return user;
|
||||
}
|
||||
} else {
|
||||
System.out.println(">>> User NOT found in DB. Checking Failsafe eligibility...");
|
||||
|
||||
// 2. Try Failsafe (Properties) - ONLY if no Master users exist in DB
|
||||
List<SystemUser> masters = repository.findByRole("MASTER");
|
||||
if (masters.isEmpty()) {
|
||||
if (email.equals(masterUsername) && password.equals(masterPassword)) {
|
||||
System.out.println(">>> FAILSAFE AUTHENTICATION SUCCESSFUL (No DB Master Found)");
|
||||
|
||||
if (email.equals(masterUsername) && passwordEncoder.matches(password, passwordEncoder.encode(masterPassword))) {
|
||||
SystemUser failsafeUser = new SystemUser();
|
||||
failsafeUser.setId(0L);
|
||||
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"));
|
||||
return Optional.of(failsafeUser);
|
||||
}
|
||||
} else {
|
||||
System.out.println(">>> Failsafe disabled because custom master account exists in database.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -132,10 +133,10 @@ public class TokenService {
|
||||
|
||||
// PERMANENT REMOVAL: Physical delete from database
|
||||
List<Long> unitIds = unitsToSpend.stream().map(TokenUnit::getId).collect(Collectors.toList());
|
||||
String deleteSql = "DELETE FROM token_units WHERE id IN (" +
|
||||
unitIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")";
|
||||
String placeholders = Collections.nCopies(unitIds.size(), "?").stream().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
|
||||
user.setRitzTokenBalance(currentBalance.subtract(amount));
|
||||
|
||||
@@ -80,7 +80,10 @@ public class UserService {
|
||||
User user = userOpt.get();
|
||||
|
||||
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())) {
|
||||
@@ -241,6 +244,20 @@ public class UserService {
|
||||
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.
|
||||
*/
|
||||
@@ -248,3 +265,4 @@ public class UserService {
|
||||
userRepository.deleteById(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user