diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 8bc0e74d..6ed1ae45 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -583,6 +583,13 @@ public String getLoginResponse(HttpServletRequest request) { throw new IEMRException("Authentication failed. Please log in again."); } + // Validate the token first + Claims claims = jwtUtil.validateToken(jwtToken); + if (claims == null) { + logger.warn("Authentication failed: invalid or expired token."); + throw new IEMRException("Authentication failed. Please log in again."); + } + // Extract user ID from the JWT token String userId = jwtUtil.getUserIdFromToken(jwtToken); @@ -1230,4 +1237,85 @@ public ResponseEntity getUserDetails(@PathVariable("userName") String userNam } } + + @Operation(summary = "Unlock user account locked due to failed login attempts") + @RequestMapping(value = "/unlockUserAccount", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String unlockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean unlocked = iemrAdminUserServiceImpl.unlockUserAccount(userId); + response.setResponse(unlocked ? "User account successfully unlocked" : "User account was not locked"); + } catch (Exception e) { + logger.error("Error unlocking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get user account lock status") + @RequestMapping(value = "/getUserLockStatus", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String getUserLockStatus(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + String lockStatusJson = iemrAdminUserServiceImpl.getUserLockStatusJson(userId); + response.setResponse(lockStatusJson); + } catch (Exception e) { + logger.error("Error getting user lock status: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + private Long parseUserIdFromRequest(String request) throws IEMRException { + try { + JsonObject requestObj = JsonParser.parseString(request).getAsJsonObject(); + if (!requestObj.has("userId") || requestObj.get("userId").isJsonNull()) { + throw new IEMRException("userId is required"); + } + JsonElement userIdElement = requestObj.get("userId"); + if (!userIdElement.isJsonPrimitive() || !userIdElement.getAsJsonPrimitive().isNumber()) { + throw new IEMRException("userId must be a number"); + } + return userIdElement.getAsLong(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + throw new IEMRException("Invalid request body", e); + } + } + + private Long getAuthenticatedUserId(HttpServletRequest httpRequest) throws IEMRException { + String authorization = httpRequest.getHeader("Authorization"); + if (authorization != null && authorization.contains("Bearer ")) { + authorization = authorization.replace("Bearer ", ""); + } + if (authorization == null || authorization.isEmpty()) { + throw new IEMRException("Authentication required"); + } + try { + String sessionJson = sessionObject.getSessionObject(authorization); + if (sessionJson == null || sessionJson.isEmpty()) { + throw new IEMRException("Session expired. Please log in again."); + } + JSONObject session = new JSONObject(sessionJson); + return session.getLong("userID"); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + throw new IEMRException("Authentication failed", e); + } + } + + private void validateAdminPrivileges(Long userId) throws IEMRException { + if (!iemrAdminUserServiceImpl.hasAdminPrivileges(userId)) { + logger.warn("Unauthorized access attempt by userId: {}", userId); + throw new IEMRException("Access denied. Admin privileges required."); + } + } } diff --git a/src/main/java/com/iemr/common/data/users/User.java b/src/main/java/com/iemr/common/data/users/User.java index 275b0ec6..677cd012 100644 --- a/src/main/java/com/iemr/common/data/users/User.java +++ b/src/main/java/com/iemr/common/data/users/User.java @@ -213,6 +213,10 @@ public class User implements Serializable { @Column(name = "dhistoken") private String dhistoken; + @Expose + @Column(name = "lock_timestamp") + private Timestamp lockTimestamp; + /* * protected User() { } */ diff --git a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java index 3ee48ab3..f10c5e03 100644 --- a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java @@ -75,7 +75,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID, @Query("SELECT u FROM User u WHERE u.userID=5718") User getAllExistingUsers(); - + User findByUserID(Long userID); } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index d7dc6e2e..28b46246 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -123,6 +123,10 @@ public List getUserServiceRoleMappingForProvider(Integ List getUserIdbyUserName(String userName) throws IEMRException; + boolean unlockUserAccount(Long userId) throws IEMRException; + + String getUserLockStatusJson(Long userId) throws IEMRException; + + boolean hasAdminPrivileges(Long userId) throws IEMRException; - } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 44bd2247..d8ed5f72 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -129,6 +129,8 @@ public class IEMRAdminUserServiceImpl implements IEMRAdminUserService { private SessionObject sessionObject; @Value("${failedLoginAttempt}") private String failedLoginAttempt; + @Value("${account.lock.duration.hours:24}") + private int accountLockDurationHours; // @Autowired // private ServiceRoleScreenMappingRepository ; @@ -221,79 +223,123 @@ public void setValidator(Validator validator) { } private void checkUserAccountStatus(User user) throws IEMRException { - if (user.getDeleted()) { - throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + if (user.getDeleted() != null && user.getDeleted()) { + if (user.getLockTimestamp() != null) { + long lockTimeMillis = user.getLockTimestamp().getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long lockDurationMillis = getLockDurationMillis(); + + if (currentTimeMillis - lockTimeMillis >= lockDurationMillis) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("User account auto-unlocked after {} hours lock period for user: {}", + accountLockDurationHours, user.getUserName()); + return; + } else { + throw new IEMRException(generateLockoutErrorMessage(user.getLockTimestamp())); + } + } else { + throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + } } else if (user.getStatusID() > 2) { throw new IEMRException("Your account is not active. Please contact administrator"); } } + /** + * Common helper method for password validation and account locking logic. + * Used by both userAuthenticate() and superUserAuthenticate(). + */ + private User handlePasswordValidationAndLocking(User user, String password, int failedAttemptThreshold) + throws IEMRException, NoSuchAlgorithmException, InvalidKeySpecException { + int validatePassword = securePassword.validatePassword(password, user.getPassword()); + + switch (validatePassword) { + case 0: + // Invalid password - handle failed attempts + handleFailedLoginAttempt(user, failedAttemptThreshold); + break; + case 1: + // Valid password with old format - upgrade to new format + checkUserAccountStatus(user); + clearFailedAttemptState(user); + user.setPassword(generateUpgradedPassword(password)); + iEMRUserRepositoryCustom.save(user); + break; + case 2: + case 3: + // Valid password + checkUserAccountStatus(user); + clearFailedAttemptState(user); + iEMRUserRepositoryCustom.save(user); + break; + default: + // Successful validation - reset failed attempts if needed + checkUserAccountStatus(user); + resetFailedAttemptsIfNeeded(user); + break; + } + return user; + } + + private void clearFailedAttemptState(User user) { + user.setFailedAttempt(0); + user.setLockTimestamp(null); + } + + private long getLockDurationMillis() { + return (long) accountLockDurationHours * 60 * 60 * 1000; + } + + private String generateUpgradedPassword(String password) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int iterations = 1001; + char[] chars = password.toCharArray(); + byte[] salt = getSalt(); + PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + byte[] hash = skf.generateSecret(spec).getEncoded(); + return iterations + ":" + toHex(salt) + ":" + toHex(hash); + } + + private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) throws IEMRException { + int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0; + if (currentAttempts + 1 < failedAttemptThreshold) { + user.setFailedAttempt(currentAttempts + 1); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); + throw new IEMRException("Invalid username or password"); + } else { + java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); + user.setFailedAttempt(currentAttempts + 1); + user.setDeleted(true); + user.setLockTimestamp(lockTime); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", + failedAttemptThreshold); + throw new IEMRException(generateLockoutErrorMessage(lockTime)); + } + } + + private void resetFailedAttemptsIfNeeded(User user) { + if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) { + clearFailedAttemptState(user); + iEMRUserRepositoryCustom.save(user); + } + } + @Override public List userAuthenticate(String userName, String password) throws Exception { List users = iEMRUserRepositoryCustom.findByUserNameNew(userName); if (users.size() != 1) { throw new IEMRException("Invalid username or password"); } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; + int failedAttemptThreshold = getFailedAttemptThreshold(); User user = users.get(0); try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { - checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 3) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -301,16 +347,37 @@ public List userAuthenticate(String userName, String password) throws Exce return users; } - private void checkUserLoginFailedAttempt(User user) throws IEMRException { - + private int getFailedAttemptThreshold() { + if (failedLoginAttempt != null && !failedLoginAttempt.trim().isEmpty()) { + try { + return Integer.parseInt(failedLoginAttempt.trim()); + } catch (NumberFormatException e) { + logger.warn("Invalid failedLoginAttempt configuration value '{}', using default of 5", failedLoginAttempt); + } + } + return 5; } - private void updateUserLoginFailedAttempt(User user) throws IEMRException { + private String generateLockoutErrorMessage(java.sql.Timestamp lockTimestamp) { + if (lockTimestamp == null) { + return "Your account has been locked. Please contact the administrator."; + } - } + long remainingMillis = calculateRemainingLockTime(lockTimestamp); - private void resetUserLoginFailedAttempt(User user) throws IEMRException { + if (remainingMillis <= 0) { + return "Your account lock has expired. Please try logging in again."; + } + return String.format("Your account has been locked. You can try again in %s, or contact the administrator.", + formatRemainingTime(remainingMillis)); + } + + private long calculateRemainingLockTime(java.sql.Timestamp lockTimestamp) { + long lockTimeMillis = lockTimestamp.getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long unlockTimeMillis = lockTimeMillis + getLockDurationMillis(); + return unlockTimeMillis - currentTimeMillis; } /** @@ -319,67 +386,13 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException { @Override public User superUserAuthenticate(String userName, String password) throws Exception { List users = iEMRUserRepositoryCustom.findByUserName(userName); - if (users.size() != 1) { throw new IEMRException("Invalid username or password"); } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; + int failedAttemptThreshold = getFailedAttemptThreshold(); User user = users.get(0); try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { - checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -1205,12 +1218,12 @@ public User getUserById(Long userId) throws IEMRException { try { // Fetch user from custom repository by userId User user = iEMRUserRepositoryCustom.findByUserID(userId); - + // Check if user is found if (user == null) { throw new IEMRException("User not found with ID: " + userId); } - + return user; } catch (Exception e) { // Log and throw custom exception in case of errors @@ -1221,7 +1234,112 @@ public User getUserById(Long userId) throws IEMRException { @Override public List getUserIdbyUserName(String userName) { - return iEMRUserRepositoryCustom.findByUserName(userName); } + + @Override + public boolean unlockUserAccount(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() != null) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually unlocked user account for userID: {}", userId); + return true; + } else if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Use user management to reactivate."); + } else { + logger.info("User account is not locked for userID: {}", userId); + return false; + } + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error unlocking user account with ID: " + userId, e); + throw new IEMRException("Error unlocking user account: " + e.getMessage(), e); + } + } + + @Override + public String getUserLockStatusJson(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + org.json.JSONObject status = new org.json.JSONObject(); + status.put("userId", user.getUserID()); + status.put("userName", user.getUserName()); + status.put("failedAttempts", user.getFailedAttempt() != null ? user.getFailedAttempt() : 0); + status.put("statusID", user.getStatusID()); + + boolean isDeleted = user.getDeleted() != null && user.getDeleted(); + boolean isLockedDueToFailedAttempts = isDeleted && user.getLockTimestamp() != null; + + status.put("isLocked", isDeleted); + status.put("isLockedDueToFailedAttempts", isLockedDueToFailedAttempts); + + if (isLockedDueToFailedAttempts) { + long remainingMillis = calculateRemainingLockTime(user.getLockTimestamp()); + boolean lockExpired = remainingMillis <= 0; + + status.put("lockExpired", lockExpired); + status.put("lockTimestamp", user.getLockTimestamp().toString()); + status.put("remainingTime", lockExpired ? "Lock expired - will unlock on next login" : formatRemainingTime(remainingMillis)); + if (!lockExpired) { + status.put("unlockTime", new java.sql.Timestamp(user.getLockTimestamp().getTime() + getLockDurationMillis()).toString()); + } + } else { + status.put("lockExpired", false); + status.put("lockTimestamp", org.json.JSONObject.NULL); + status.put("remainingTime", org.json.JSONObject.NULL); + } + + return status.toString(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error fetching user lock status with ID: " + userId, e); + throw new IEMRException("Error fetching user lock status: " + e.getMessage(), e); + } + } + + private String formatRemainingTime(long remainingMillis) { + long hours = remainingMillis / (60 * 60 * 1000); + long minutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000); + if (hours > 0 && minutes > 0) return String.format("%d hours %d minutes", hours, minutes); + if (hours > 0) return String.format("%d hours", hours); + return String.format("%d minutes", minutes); + } + + @Override + public boolean hasAdminPrivileges(Long userId) throws IEMRException { + try { + List roleMappings = getUserServiceRoleMapping(userId); + if (roleMappings == null || roleMappings.isEmpty()) { + return false; + } + for (UserServiceRoleMapping mapping : roleMappings) { + Role role = mapping.getM_Role(); + if (role != null && role.getRoleName() != null) { + String roleName = role.getRoleName().toLowerCase(); + if (roleName.contains("admin") || roleName.contains("supervisor") || roleName.contains("provideradmin")) { + return true; + } + } + } + return false; + } catch (Exception e) { + logger.error("Error checking admin privileges for userId: " + userId, e); + return false; + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18723465..c0d0ccc6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -169,6 +169,9 @@ quality-Audit-PageSize=5 ## max no of failed login attempt failedLoginAttempt=5 +## account lock duration in hours (24 hours = 1 day for auto-unlock) +account.lock.duration.hours=24 + #Jwt Token configuration jwt.access.expiration=28800000 jwt.refresh.expiration=604800000