Skip to content

Commit ffdf91b

Browse files
author
Varun Deep Saini
committed
AMM-118: Add time-based account lockout with auto-unlock
1 parent a1a0027 commit ffdf91b

7 files changed

Lines changed: 208 additions & 36 deletions

File tree

bin

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 9ffa450d1a5f73d42e60582b36442f0e1619e438

src/main/java/com/iemr/common/controller/users/IEMRAdminController.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,4 +1230,42 @@ public ResponseEntity<?> getUserDetails(@PathVariable("userName") String userNam
12301230
}
12311231

12321232
}
1233+
1234+
@Operation(summary = "Unlock user account locked due to failed login attempts")
1235+
@RequestMapping(value = "/unlockUserAccount", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization")
1236+
public String unlockUserAccount(@RequestBody String request) {
1237+
OutputResponse response = new OutputResponse();
1238+
try {
1239+
Long userId = parseUserIdFromRequest(request);
1240+
boolean unlocked = iemrAdminUserServiceImpl.unlockUserAccount(userId);
1241+
response.setResponse(unlocked ? "User account successfully unlocked" : "User account was not locked");
1242+
} catch (Exception e) {
1243+
logger.error("Error unlocking user account: " + e.getMessage(), e);
1244+
response.setError(e);
1245+
}
1246+
return response.toString();
1247+
}
1248+
1249+
@Operation(summary = "Get user account lock status")
1250+
@RequestMapping(value = "/getUserLockStatus", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization")
1251+
public String getUserLockStatus(@RequestBody String request) {
1252+
OutputResponse response = new OutputResponse();
1253+
try {
1254+
Long userId = parseUserIdFromRequest(request);
1255+
String lockStatusJson = iemrAdminUserServiceImpl.getUserLockStatusJson(userId);
1256+
response.setResponse(lockStatusJson);
1257+
} catch (Exception e) {
1258+
logger.error("Error getting user lock status: " + e.getMessage(), e);
1259+
response.setError(e);
1260+
}
1261+
return response.toString();
1262+
}
1263+
1264+
private Long parseUserIdFromRequest(String request) throws IEMRException {
1265+
JsonObject requestObj = JsonParser.parseString(request).getAsJsonObject();
1266+
if (!requestObj.has("userId") || requestObj.get("userId").isJsonNull()) {
1267+
throw new IEMRException("userId is required");
1268+
}
1269+
return requestObj.get("userId").getAsLong();
1270+
}
12331271
}

src/main/java/com/iemr/common/data/users/User.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ public class User implements Serializable {
213213
@Column(name = "dhistoken")
214214
private String dhistoken;
215215

216+
@Expose
217+
@Column(name = "lock_timestamp")
218+
private Timestamp lockTimestamp;
219+
216220
/*
217221
* protected User() { }
218222
*/

src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID,
7575

7676
@Query("SELECT u FROM User u WHERE u.userID=5718")
7777
User getAllExistingUsers();
78-
78+
7979
User findByUserID(Long userID);
8080

8181
}

src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ public List<ServiceRoleScreenMapping> getUserServiceRoleMappingForProvider(Integ
123123

124124
List<User> getUserIdbyUserName(String userName) throws IEMRException;
125125

126+
boolean unlockUserAccount(Long userId) throws IEMRException;
127+
128+
String getUserLockStatusJson(Long userId) throws IEMRException;
126129

127-
128130
}

src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java

Lines changed: 158 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ public class IEMRAdminUserServiceImpl implements IEMRAdminUserService {
129129
private SessionObject sessionObject;
130130
@Value("${failedLoginAttempt}")
131131
private String failedLoginAttempt;
132+
@Value("${account.lock.duration.hours:24}")
133+
private int accountLockDurationHours;
132134
// @Autowired
133135
// private ServiceRoleScreenMappingRepository ;
134136

@@ -222,7 +224,25 @@ public void setValidator(Validator validator) {
222224

223225
private void checkUserAccountStatus(User user) throws IEMRException {
224226
if (user.getDeleted()) {
225-
throw new IEMRException("Your account is locked or de-activated. Please contact administrator");
227+
if (user.getLockTimestamp() != null) {
228+
long lockTimeMillis = user.getLockTimestamp().getTime();
229+
long currentTimeMillis = System.currentTimeMillis();
230+
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;
231+
232+
if (currentTimeMillis - lockTimeMillis >= lockDurationMillis) {
233+
user.setDeleted(false);
234+
user.setFailedAttempt(0);
235+
user.setLockTimestamp(null);
236+
iEMRUserRepositoryCustom.save(user);
237+
logger.info("User account auto-unlocked after {} hours lock period for user: {}",
238+
accountLockDurationHours, user.getUserName());
239+
return;
240+
} else {
241+
throw new IEMRException(generateLockoutErrorMessage(user.getLockTimestamp()));
242+
}
243+
} else {
244+
throw new IEMRException("Your account is locked or de-activated. Please contact administrator");
245+
}
226246
} else if (user.getStatusID() > 2) {
227247
throw new IEMRException("Your account is not active. Please contact administrator");
228248
}
@@ -265,32 +285,27 @@ public List<User> userAuthenticate(String userName, String password) throws Exce
265285
checkUserAccountStatus(user);
266286
iEMRUserRepositoryCustom.save(user);
267287
} else if (validatePassword == 0) {
268-
if (user.getFailedAttempt() + 1 < failedAttempt) {
269-
user.setFailedAttempt(user.getFailedAttempt() + 1);
288+
int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0;
289+
if (currentAttempts + 1 < failedAttempt) {
290+
user.setFailedAttempt(currentAttempts + 1);
270291
user = iEMRUserRepositoryCustom.save(user);
271292
logger.warn("User Password Wrong");
272293
throw new IEMRException("Invalid username or password");
273-
} else if (user.getFailedAttempt() + 1 >= failedAttempt) {
274-
user.setFailedAttempt(user.getFailedAttempt() + 1);
294+
} else {
295+
java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis());
296+
user.setFailedAttempt(currentAttempts + 1);
275297
user.setDeleted(true);
298+
user.setLockTimestamp(lockTime);
276299
user = iEMRUserRepositoryCustom.save(user);
277300
logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.",
278301
ConfigProperties.getInteger("failedLoginAttempt"));
279-
280-
throw new IEMRException(
281-
"Invalid username or password. Please contact administrator.");
282-
} else {
283-
user.setFailedAttempt(user.getFailedAttempt() + 1);
284-
user = iEMRUserRepositoryCustom.save(user);
285-
logger.warn("Failed login attempt {} of {} for a user account.",
286-
user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt"));
287-
throw new IEMRException(
288-
"Invalid username or password. Please contact administrator.");
302+
throw new IEMRException(generateLockoutErrorMessage(lockTime));
289303
}
290304
} else {
291305
checkUserAccountStatus(user);
292-
if (user.getFailedAttempt() != 0) {
306+
if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) {
293307
user.setFailedAttempt(0);
308+
user.setLockTimestamp(null);
294309
user = iEMRUserRepositoryCustom.save(user);
295310
}
296311
}
@@ -313,6 +328,37 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException {
313328

314329
}
315330

331+
private String generateLockoutErrorMessage(java.sql.Timestamp lockTimestamp) {
332+
if (lockTimestamp == null) {
333+
return "Your account has been locked. Please contact the administrator.";
334+
}
335+
336+
long lockTimeMillis = lockTimestamp.getTime();
337+
long currentTimeMillis = System.currentTimeMillis();
338+
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;
339+
long unlockTimeMillis = lockTimeMillis + lockDurationMillis;
340+
long remainingMillis = unlockTimeMillis - currentTimeMillis;
341+
342+
if (remainingMillis <= 0) {
343+
return "Your account lock has expired. Please try logging in again.";
344+
}
345+
346+
long remainingHours = remainingMillis / (60 * 60 * 1000);
347+
long remainingMinutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000);
348+
349+
String timeMessage;
350+
if (remainingHours > 0 && remainingMinutes > 0) {
351+
timeMessage = String.format("%d hours %d minutes", remainingHours, remainingMinutes);
352+
} else if (remainingHours > 0) {
353+
timeMessage = String.format("%d hours", remainingHours);
354+
} else {
355+
timeMessage = String.format("%d minutes", remainingMinutes);
356+
}
357+
358+
return String.format("Your account has been locked. You can try again in %s, or contact the administrator.", timeMessage);
359+
360+
}
361+
316362
/**
317363
* Super Admin login
318364
*/
@@ -351,32 +397,27 @@ public User superUserAuthenticate(String userName, String password) throws Excep
351397
iEMRUserRepositoryCustom.save(user);
352398

353399
} else if (validatePassword == 0) {
354-
if (user.getFailedAttempt() + 1 < failedAttempt) {
355-
user.setFailedAttempt(user.getFailedAttempt() + 1);
400+
int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0;
401+
if (currentAttempts + 1 < failedAttempt) {
402+
user.setFailedAttempt(currentAttempts + 1);
356403
user = iEMRUserRepositoryCustom.save(user);
357404
logger.warn("User Password Wrong");
358405
throw new IEMRException("Invalid username or password");
359-
} else if (user.getFailedAttempt() + 1 >= failedAttempt) {
360-
user.setFailedAttempt(user.getFailedAttempt() + 1);
406+
} else {
407+
java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis());
408+
user.setFailedAttempt(currentAttempts + 1);
361409
user.setDeleted(true);
410+
user.setLockTimestamp(lockTime);
362411
user = iEMRUserRepositoryCustom.save(user);
363412
logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.",
364413
ConfigProperties.getInteger("failedLoginAttempt"));
365-
366-
throw new IEMRException(
367-
"Invalid username or password. Please contact administrator.");
368-
} else {
369-
user.setFailedAttempt(user.getFailedAttempt() + 1);
370-
user = iEMRUserRepositoryCustom.save(user);
371-
logger.warn("Failed login attempt {} of {} for a user account.",
372-
user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt"));
373-
throw new IEMRException(
374-
"Invalid username or password. Please contact administrator.");
414+
throw new IEMRException(generateLockoutErrorMessage(lockTime));
375415
}
376416
} else {
377417
checkUserAccountStatus(user);
378-
if (user.getFailedAttempt() != 0) {
418+
if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) {
379419
user.setFailedAttempt(0);
420+
user.setLockTimestamp(null);
380421
user = iEMRUserRepositoryCustom.save(user);
381422
}
382423
}
@@ -1205,12 +1246,12 @@ public User getUserById(Long userId) throws IEMRException {
12051246
try {
12061247
// Fetch user from custom repository by userId
12071248
User user = iEMRUserRepositoryCustom.findByUserID(userId);
1208-
1249+
12091250
// Check if user is found
12101251
if (user == null) {
12111252
throw new IEMRException("User not found with ID: " + userId);
12121253
}
1213-
1254+
12141255
return user;
12151256
} catch (Exception e) {
12161257
// Log and throw custom exception in case of errors
@@ -1221,7 +1262,90 @@ public User getUserById(Long userId) throws IEMRException {
12211262

12221263
@Override
12231264
public List<User> getUserIdbyUserName(String userName) {
1224-
12251265
return iEMRUserRepositoryCustom.findByUserName(userName);
12261266
}
1267+
1268+
@Override
1269+
public boolean unlockUserAccount(Long userId) throws IEMRException {
1270+
try {
1271+
User user = iEMRUserRepositoryCustom.findById(userId).orElse(null);
1272+
1273+
if (user == null) {
1274+
throw new IEMRException("User not found with ID: " + userId);
1275+
}
1276+
1277+
if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() != null) {
1278+
user.setDeleted(false);
1279+
user.setFailedAttempt(0);
1280+
user.setLockTimestamp(null);
1281+
iEMRUserRepositoryCustom.save(user);
1282+
logger.info("Admin manually unlocked user account for userID: {}", userId);
1283+
return true;
1284+
} else if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() == null) {
1285+
throw new IEMRException("User account is deactivated by administrator. Use user management to reactivate.");
1286+
} else {
1287+
logger.info("User account is not locked for userID: {}", userId);
1288+
return false;
1289+
}
1290+
} catch (IEMRException e) {
1291+
throw e;
1292+
} catch (Exception e) {
1293+
logger.error("Error unlocking user account with ID: " + userId, e);
1294+
throw new IEMRException("Error unlocking user account: " + e.getMessage(), e);
1295+
}
1296+
}
1297+
1298+
@Override
1299+
public String getUserLockStatusJson(Long userId) throws IEMRException {
1300+
try {
1301+
User user = iEMRUserRepositoryCustom.findById(userId).orElse(null);
1302+
if (user == null) {
1303+
throw new IEMRException("User not found with ID: " + userId);
1304+
}
1305+
1306+
org.json.JSONObject status = new org.json.JSONObject();
1307+
status.put("userId", user.getUserID());
1308+
status.put("userName", user.getUserName());
1309+
status.put("failedAttempts", user.getFailedAttempt() != null ? user.getFailedAttempt() : 0);
1310+
status.put("statusID", user.getStatusID());
1311+
1312+
boolean isDeleted = user.getDeleted() != null && user.getDeleted();
1313+
boolean isLockedDueToFailedAttempts = isDeleted && user.getLockTimestamp() != null;
1314+
1315+
status.put("isLocked", isDeleted);
1316+
status.put("isLockedDueToFailedAttempts", isLockedDueToFailedAttempts);
1317+
1318+
if (isLockedDueToFailedAttempts) {
1319+
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;
1320+
long remainingMillis = (user.getLockTimestamp().getTime() + lockDurationMillis) - System.currentTimeMillis();
1321+
boolean lockExpired = remainingMillis <= 0;
1322+
1323+
status.put("lockExpired", lockExpired);
1324+
status.put("lockTimestamp", user.getLockTimestamp().toString());
1325+
status.put("remainingTime", lockExpired ? "Lock expired - will unlock on next login" : formatRemainingTime(remainingMillis));
1326+
if (!lockExpired) {
1327+
status.put("unlockTime", new java.sql.Timestamp(user.getLockTimestamp().getTime() + lockDurationMillis).toString());
1328+
}
1329+
} else {
1330+
status.put("lockExpired", false);
1331+
status.put("lockTimestamp", org.json.JSONObject.NULL);
1332+
status.put("remainingTime", org.json.JSONObject.NULL);
1333+
}
1334+
1335+
return status.toString();
1336+
} catch (IEMRException e) {
1337+
throw e;
1338+
} catch (Exception e) {
1339+
logger.error("Error fetching user lock status with ID: " + userId, e);
1340+
throw new IEMRException("Error fetching user lock status: " + e.getMessage(), e);
1341+
}
1342+
}
1343+
1344+
private String formatRemainingTime(long remainingMillis) {
1345+
long hours = remainingMillis / (60 * 60 * 1000);
1346+
long minutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000);
1347+
if (hours > 0 && minutes > 0) return String.format("%d hours %d minutes", hours, minutes);
1348+
if (hours > 0) return String.format("%d hours", hours);
1349+
return String.format("%d minutes", minutes);
1350+
}
12271351
}

src/main/resources/application.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ quality-Audit-PageSize=5
169169
## max no of failed login attempt
170170
failedLoginAttempt=5
171171

172+
## account lock duration in hours (24 hours = 1 day for auto-unlock)
173+
account.lock.duration.hours=24
174+
172175
#Jwt Token configuration
173176
jwt.access.expiration=28800000
174177
jwt.refresh.expiration=604800000

0 commit comments

Comments
 (0)