From 33ad97145046b2cd35a294a4e806941768aef4b3 Mon Sep 17 00:00:00 2001 From: neha222222 Date: Mon, 13 Apr 2026 02:58:15 +0530 Subject: [PATCH 1/2] implement time-bound account lock with auto-unlock after 24h - added lock_timestamp column to m_user table - on 5th failed login, account gets locked with timestamp instead of permanent block - on next login attempt after 24h, account auto-unlocks (resets deleted flag, failed attempts, and lock timestamp) - updated error message: "Your account has been locked. You can try tomorrow or connect to the administrator." - failed attempt counter resets on successful login fixes PSMRI/AMRIT#118 --- .../java/com/iemr/common/data/users/User.java | 4 +++ .../users/IEMRAdminUserServiceImpl.java | 33 ++++++++++++++++--- .../V1__add_lock_timestamp_to_m_user.sql | 5 +++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/db/migration/V1__add_lock_timestamp_to_m_user.sql 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..cf0d19ac 100644 --- a/src/main/java/com/iemr/common/data/users/User.java +++ b/src/main/java/com/iemr/common/data/users/User.java @@ -209,6 +209,10 @@ public class User implements Serializable { @Column(name = "failed_attempt") private Integer failedAttempt; + @Expose + @Column(name = "lock_timestamp") + private Timestamp lockTimestamp; + @Expose @Column(name = "dhistoken") private String dhistoken; 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 71d72c97..9c4cdb44 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -222,7 +222,27 @@ 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"); + // check if account was locked due to failed attempts and lock has expired + if (user.getLockTimestamp() != null) { + long lockTime = user.getLockTimestamp().getTime(); + long now = System.currentTimeMillis(); + long twentyFourHoursMs = 24 * 60 * 60 * 1000L; + + if (now - lockTime >= twentyFourHoursMs) { + // auto-unlock: lock period expired + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("Account auto-unlocked for user {} after 24h lock period expired", user.getUserName()); + return; + } else { + throw new IEMRException( + "Your account has been locked. You can try tomorrow or connect to the administrator."); + } + } + 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"); } @@ -273,24 +293,26 @@ public List userAuthenticate(String userName, String password) throws Exce } else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); + user.setLockTimestamp(new java.sql.Timestamp(System.currentTimeMillis())); 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."); + "Your account has been locked. You can try tomorrow or connect to the 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."); + "Invalid username or password."); } } else { checkUserAccountStatus(user); if (user.getFailedAttempt() != 0) { user.setFailedAttempt(0); + user.setLockTimestamp(null); user = iEMRUserRepositoryCustom.save(user); } } @@ -359,19 +381,20 @@ public User superUserAuthenticate(String userName, String password) throws Excep } else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); + user.setLockTimestamp(new java.sql.Timestamp(System.currentTimeMillis())); 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."); + "Your account has been locked. You can try tomorrow or connect to the 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."); + "Invalid username or password."); } } else { checkUserAccountStatus(user); diff --git a/src/main/resources/db/migration/V1__add_lock_timestamp_to_m_user.sql b/src/main/resources/db/migration/V1__add_lock_timestamp_to_m_user.sql new file mode 100644 index 00000000..b59f69c8 --- /dev/null +++ b/src/main/resources/db/migration/V1__add_lock_timestamp_to_m_user.sql @@ -0,0 +1,5 @@ +-- Add lock_timestamp column to m_user table for time-bound account locking +-- When a user gets locked after 5 failed login attempts, this stores when +-- the lock happened. After 24 hours, the account auto-unlocks on next login. + +ALTER TABLE m_user ADD COLUMN lock_timestamp DATETIME NULL DEFAULT NULL; From 799c2143037e78b0ee04b1150d0750629a7453c5 Mon Sep 17 00:00:00 2001 From: neha222222 Date: Mon, 13 Apr 2026 04:46:57 +0530 Subject: [PATCH 2/2] fix review feedback: calendar-day unlock, guard locked users, superuser query - use LocalDate comparison instead of rolling 24h so accounts unlock at midnight as the error message promises - guard against overwriting lockTimestamp when an already-locked user enters wrong password again - switch superUserAuthenticate to findByUserNameNew so temporarily locked superusers can still be loaded and auto-unlocked - clear lockTimestamp on successful superuser login too --- .../users/IEMRAdminUserServiceImpl.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 9c4cdb44..021ab74c 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -224,17 +224,17 @@ private void checkUserAccountStatus(User user) throws IEMRException { if (user.getDeleted()) { // check if account was locked due to failed attempts and lock has expired if (user.getLockTimestamp() != null) { - long lockTime = user.getLockTimestamp().getTime(); - long now = System.currentTimeMillis(); - long twentyFourHoursMs = 24 * 60 * 60 * 1000L; + java.time.LocalDate lockDate = user.getLockTimestamp().toLocalDateTime().toLocalDate(); + java.time.LocalDate today = java.time.LocalDate.now(); - if (now - lockTime >= twentyFourHoursMs) { - // auto-unlock: lock period expired + if (today.isAfter(lockDate)) { + // auto-unlock: it's a new day after the lock date user.setDeleted(false); user.setFailedAttempt(0); user.setLockTimestamp(null); iEMRUserRepositoryCustom.save(user); - logger.info("Account auto-unlocked for user {} after 24h lock period expired", user.getUserName()); + logger.info("Account auto-unlocked for user {} (locked on {}, unlocked on {})", + user.getUserName(), lockDate, today); return; } else { throw new IEMRException( @@ -285,6 +285,10 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { + // if already locked/deleted, don't overwrite lock timestamp + if (Boolean.TRUE.equals(user.getDeleted())) { + checkUserAccountStatus(user); + } if (user.getFailedAttempt() + 1 < failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); @@ -340,7 +344,7 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException { */ @Override public User superUserAuthenticate(String userName, String password) throws Exception { - List users = iEMRUserRepositoryCustom.findByUserName(userName); + List users = iEMRUserRepositoryCustom.findByUserNameNew(userName); if (users.size() != 1) { throw new IEMRException("Invalid username or password"); @@ -373,6 +377,10 @@ public User superUserAuthenticate(String userName, String password) throws Excep iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { + // if already locked/deleted, don't overwrite lock timestamp + if (Boolean.TRUE.equals(user.getDeleted())) { + checkUserAccountStatus(user); + } if (user.getFailedAttempt() + 1 < failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); @@ -400,6 +408,7 @@ public User superUserAuthenticate(String userName, String password) throws Excep checkUserAccountStatus(user); if (user.getFailedAttempt() != 0) { user.setFailedAttempt(0); + user.setLockTimestamp(null); user = iEMRUserRepositoryCustom.save(user); } }