Skip to content

fix: implement 24-hour auto-unlock for account lockout after failed l…#408

Open
Aarti-panchal01 wants to merge 2 commits into
PSMRI:mainfrom
Aarti-panchal01:fix/account-lockout-auto-unlock-24hrs
Open

fix: implement 24-hour auto-unlock for account lockout after failed l…#408
Aarti-panchal01 wants to merge 2 commits into
PSMRI:mainfrom
Aarti-panchal01:fix/account-lockout-auto-unlock-24hrs

Conversation

@Aarti-panchal01
Copy link
Copy Markdown

@Aarti-panchal01 Aarti-panchal01 commented May 12, 2026

📋 Description

Fixes: PSMRI/AMRIT#118

Currently, when a user enters an incorrect password 5 times, the account is permanently blocked with no automatic recovery mechanism. This forces manual admin intervention for every lockout.

This PR implements a time-bound 24-hour account lockout mechanism as described in the issue.


✅ Type of Change

  • 🐛 Bug fix (non-breaking change which resolves an issue)
  • New feature (non-breaking change which adds functionality)

🔧 Changes Made

User.java

  • Added lockedAt Timestamp field to track when account was locked

IEMRAdminUserServiceImpl.java

  • checkUserLoginFailedAttempt(): checks if 24 hours elapsed since lock, auto-unlocks if yes
  • updateUserLoginFailedAttempt(): records lockedAt timestamp when 5th failed attempt occurs
  • resetUserLoginFailedAttempt(): clears lockedAt on successful login
  • superUserAuthenticate(): integrated auto-unlock check before each login attempt

ℹ️ Additional Information

How it works:

  1. User enters wrong password 5 times → account locked, lockedAt timestamp recorded
  2. On next login attempt → system checks if 24 hours have passed
  3. If yes → account auto-unlocks, counter resets, user can log in
  4. If no → user remains locked
  5. Successful login at any point resets the counter and clears lockedAt

Note: A DB migration adding the locked_at column to the users table will be required before deployment.

Summary by CodeRabbit

  • New Features
    • Accounts automatically unlock after 24 hours if locked due to failed login attempts.
    • Enhanced failed-login tracking with automatic account locking after reaching maximum failed attempts.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Warning

Rate limit exceeded

@Aarti-panchal01 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 37 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef6ae807-c35d-4880-bd2d-2757aa5ec63b

📥 Commits

Reviewing files that changed from the base of the PR and between 9a54398 and bb3dad4.

📒 Files selected for processing (2)
  • src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
  • src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java
📝 Walkthrough

Walkthrough

This PR introduces time-bound account lockout with automatic unlock after 24 hours. The User entity gains a lockedAt timestamp field, and the authentication service adds helper methods to check for auto-unlock eligibility, track failed attempts, and reset attempt counters. The superUserAuthenticate method is refactored to call these helpers instead of inline lock/unlock logic.

Changes

Account Lockout and Auto-Unlock Feature

Layer / File(s) Summary
User entity lockout timestamp
src/main/java/com/iemr/common/data/users/User.java
User gains a persisted lockedAt timestamp property mapped to the locked_at column, with explicit getter and setter methods.
Account auto-unlock and failed-attempt tracking
src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java
Three new private helper methods manage account locking: checkUserLoginFailedAttempt checks whether a locked account should auto-unlock after 24 hours; updateUserLoginFailedAttempt increments failed attempts and locks the account when a configured threshold is reached; resetUserLoginFailedAttempt clears both failed-attempt count and lock timestamp upon successful login. The superUserAuthenticate method is updated to call the auto-unlock check before password validation, reset failed-attempts after successful validation, and invoke the lock helper on password validation failure.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A timestamp we add, to bind the lock's fate,
Twenty-four hours later, the gate opens straight!
Failed logins now tracked with helper calls clean,
Auto-unlock blooms—the fairest we've seen.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements core backend requirements: time-bound locking (24-hour duration), auto-unlock check during login, and failed-attempt counter reset, but does not address frontend requirements like error message updates or Admin Panel visibility. Complete remaining objectives: update error message to include unlock timing, implement Admin Panel display of lock status, and enable manual unlock functionality from admin interface.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing 24-hour auto-unlock for account lockout after failed login attempts, which is the core objective of this PR.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the 24-hour auto-unlock mechanism; no out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Aarti-panchal01
Copy link
Copy Markdown
Author

Hi @drtechie,

I've implemented the 24-hour auto-unlock mechanism for the account lockout issue referenced in PSMRI/AMRIT#118.

I'm applying for C4GT DMP 2026 with Piramal Swasthya — specifically interested in the MMU-UI migration (#130), Helpline104-UI (#129), and FLW Gamification (#132) projects. This PR demonstrates my interest in contributing to AMRIT.

Happy to make any changes based on your review feedback.

Thank you!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java (2)

223-229: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update error message per requirements.

The PR objectives specify updating the user-facing error message to: "Your account has been locked. You can try tomorrow or connect to the administrator."

However, the current error message at line 225 remains:

"Your account is locked or de-activated. Please contact administrator"

This message should be updated to inform users about the time-bound nature of the lockout and provide clearer guidance.

📝 Proposed fix for error message
 	private void checkUserAccountStatus(User user) throws IEMRException {
 		if (user.getDeleted()) {
-			throw new IEMRException("Your account is locked or de-activated. Please contact administrator");
+			throw new IEMRException("Your account has been locked. You can try tomorrow or connect to the administrator.");
 		} else if (user.getStatusID() > 2) {
 			throw new IEMRException("Your account is not active. Please contact administrator");
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java`
around lines 223 - 229, In checkUserAccountStatus(User user) update the thrown
IEMRException message for the deleted/account-locked case: replace the current
message thrown in the if (user.getDeleted()) branch with "Your account has been
locked. You can try tomorrow or connect to the administrator." Leave the other
branch (user.getStatusID() > 2) unchanged.

232-302: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Inconsistent and incomplete lockout logic across authentication methods creates security vulnerability.

The three authentication methods have conflicting lockout implementations:

  • userAuthenticate (lines 232-302): Uses old inline lockout logic without 24-hour auto-unlock
  • superUserAuthenticate (lines 357-409): Uses refactored helper methods (checkUserLoginFailedAttempt, resetUserLoginFailedAttempt, updateUserLoginFailedAttempt) with 24-hour auto-unlock
  • userAuthenticateByEncryption (lines 1090-1108): Has no lockout logic at all

Since all three methods are actively used in production (verified in IEMRAdminController), this creates:

  1. Account lockout bypass: Attackers can authenticate via userAuthenticateByEncryption even if the account is locked via another method
  2. Inconsistent security behavior: Only super users get auto-unlock protection
  3. Maintenance risk: Lockout logic duplicated across methods without synchronization

Update userAuthenticate and userAuthenticateByEncryption to use the same helper methods as superUserAuthenticate to ensure consistent lockout behavior across all authentication paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java`
around lines 232 - 302, The userAuthenticate and userAuthenticateByEncryption
methods must be refactored to use the same lockout helpers as
superUserAuthenticate to remove duplicated/inconsistent inline logic: replace
the inline failed-attempt handling in userAuthenticate (the validatePassword ==
0 branch and the surrounding failedAttempt calculations) with calls to
checkUserLoginFailedAttempt(user), updateUserLoginFailedAttempt(user) and
resetUserLoginFailedAttempt(user) as appropriate around successful/failed
validation, and do the same for userAuthenticateByEncryption; retain
checkUserAccountStatus(user) where present, remove the ad-hoc manipulation of
user.setFailedAttempt()/user.setDeleted() and ConfigProperties references in
these two methods, and ensure after successful authentication you call
resetUserLoginFailedAttempt(user) and persist via
iEMRUserRepositoryCustom.save(user) as superUserAuthenticate does.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java`:
- Around line 304-320: Add a manual admin unlock: implement a new service method
adminUnlockAccount(Long userId) in IEMRAdminUserServiceImpl that looks up the
user via iEMRUserRepositoryCustom.findUserByUserID(userId), returns/throws if
not found, sets user.setDeleted(false), user.setFailedAttempt(0),
user.setLockedAt(null), saves with iEMRUserRepositoryCustom.save(user) and logs
via logger.info("Admin unlocked user account. User ID: {}", userId);
declare/throw IEMRException as needed. Expose a controller endpoint in
IEMRAdminController (e.g., `@PostMapping`("/unlockUserAccount")) which accepts a
request containing userID, validates input, calls
iemrAdminUserServiceImpl.adminUnlockAccount(userId), handles exceptions and
returns an appropriate success/failure response. Ensure null checks, proper
exception handling, and permission (admin) guard where applicable.

---

Outside diff comments:
In `@src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java`:
- Around line 223-229: In checkUserAccountStatus(User user) update the thrown
IEMRException message for the deleted/account-locked case: replace the current
message thrown in the if (user.getDeleted()) branch with "Your account has been
locked. You can try tomorrow or connect to the administrator." Leave the other
branch (user.getStatusID() > 2) unchanged.
- Around line 232-302: The userAuthenticate and userAuthenticateByEncryption
methods must be refactored to use the same lockout helpers as
superUserAuthenticate to remove duplicated/inconsistent inline logic: replace
the inline failed-attempt handling in userAuthenticate (the validatePassword ==
0 branch and the surrounding failedAttempt calculations) with calls to
checkUserLoginFailedAttempt(user), updateUserLoginFailedAttempt(user) and
resetUserLoginFailedAttempt(user) as appropriate around successful/failed
validation, and do the same for userAuthenticateByEncryption; retain
checkUserAccountStatus(user) where present, remove the ad-hoc manipulation of
user.setFailedAttempt()/user.setDeleted() and ConfigProperties references in
these two methods, and ensure after successful authentication you call
resetUserLoginFailedAttempt(user) and persist via
iEMRUserRepositoryCustom.save(user) as superUserAuthenticate does.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6affa3ed-d9fa-4489-aab3-61db4380f5a3

📥 Commits

Reviewing files that changed from the base of the PR and between 1c69049 and 9a54398.

📒 Files selected for processing (2)
  • src/main/java/com/iemr/common/data/users/User.java
  • src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java

Comment on lines 304 to 320
private void checkUserLoginFailedAttempt(User user) throws IEMRException {

if (user.getDeleted() != null && user.getDeleted() && user.getLockedAt() != null) {
long lockedTime = user.getLockedAt().getTime();
long currentTime = System.currentTimeMillis();
long diffInMillis = currentTime - lockedTime;
long diffInHours = diffInMillis / (1000 * 60 * 60);

// If 24 hours have passed, auto-unlock the account
if (diffInHours >= 24) {
user.setDeleted(false);
user.setFailedAttempt(0);
user.setLockedAt(null);
iEMRUserRepositoryCustom.save(user);
logger.info("User account auto-unlocked after 24 hours. User ID: {}", user.getUserID());
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for admin unlock or account unlock endpoints
echo "=== Searching for unlock-related endpoints ==="
rg -n -i 'unlock|unlockaccount' --type=java -C3

echo -e "\n=== Searching for admin user management endpoints ==="
rg -n -i '@PostMapping.*admin.*user|@PutMapping.*admin.*user' --type=java -C3

echo -e "\n=== Checking if deleted field is updated via admin endpoints ==="
rg -n 'setDeleted\(false\)' --type=java -C3

Repository: PSMRI/Common-API

Length of output: 27199


🏁 Script executed:

# Search for admin controller files
echo "=== Looking for admin controller files ==="
fd -type f -name "*Admin*Controller*" src/

echo -e "\n=== Searching for updateUser or similar admin endpoints ==="
rg -n 'public.*User.*update|@PutMapping|@PostMapping' src/main/java/com/iemr/common/controller/users/ -A3 --type=java

echo -e "\n=== Checking for methods that set deleted=false for users (admin context) ==="
rg -n 'public.*setDeleted|unlockAccount|unlockUser' src/main/java/com/iemr/common/service/users/ --type=java -A5

echo -e "\n=== Search for any endpoint mapping patterns for user unlock/update ==="
rg -n '@RequestMapping|@GetMapping|@PostMapping|@PutMapping|@DeleteMapping' src/main/java/com/iemr/common/controller/users/IEMRAdminController.java --type=java | head -30

Repository: PSMRI/Common-API

Length of output: 4397


🏁 Script executed:

# Find the actual admin controller file
echo "=== Finding admin controller file ==="
find src -name "*IEMRAdminController*" -type f

echo -e "\n=== Check the service methods available in IEMRAdminUserServiceImpl ==="
rg -n 'public.*User|public.*save|public.*update' src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java --type=java | head -40

echo -e "\n=== Check if there's an admin update user method ==="
cat src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java | head -100

echo -e "\n=== List all public methods in IEMRAdminUserServiceImpl ==="
rg -n '^[[:space:]]*public' src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java --type=java

Repository: PSMRI/Common-API

Length of output: 11309


🏁 Script executed:

# Get all endpoints in IEMRAdminController
echo "=== All RequestMapping methods in IEMRAdminController ==="
rg -n '@RequestMapping|@PostMapping|@PutMapping|@GetMapping|@DeleteMapping' src/main/java/com/iemr/common/controller/users/IEMRAdminController.java --type=java -A2 | head -80

echo -e "\n=== Search for any user update/save/modify endpoint in the controller ==="
rg -n 'updateUser|saveUser|modifyUser|unlockAccount|adminUnlock' src/main/java/com/iemr/common/controller/users/IEMRAdminController.java --type=java -C3

echo -e "\n=== Check entire controller file size ==="
wc -l src/main/java/com/iemr/common/controller/users/IEMRAdminController.java

Repository: PSMRI/Common-API

Length of output: 7196


🏁 Script executed:

# Check the rest of the controller file for update endpoints
echo "=== Search in entire controller for any endpoint that might update user ==="
rg -n 'method = RequestMethod.PUT|method = RequestMethod.PATCH|@PutMapping|@PatchMapping' src/main/java/com/iemr/common/controller/users/IEMRAdminController.java --type=java

echo -e "\n=== Check all RequestMapping values (full list) ==="
rg -n '@RequestMapping\(value = "' src/main/java/com/iemr/common/controller/users/IEMRAdminController.java --type=java | tail -20

echo -e "\n=== Check if there's any user admin API/endpoint file ==="
find src -name "*AdminAPI*" -o -name "*UserAdmin*" -o -name "*AdminUser*" | grep -i controller

echo -e "\n=== Final comprehensive search for admin-related endpoints ==="
rg -rn 'class.*Admin.*Controller' src/main/java/com/iemr/common/controller/ --type=java

Repository: PSMRI/Common-API

Length of output: 3272


🏁 Script executed:

# One final comprehensive check for any user update capability
echo "=== Search all Java files for any unlock or admin user update methods ==="
rg -n 'adminUnlock|manualUnlock|unlockUser|unlockAccount' src/main/java --type=java

echo -e "\n=== Check if User model has any unlock-related methods ==="
rg -n 'public.*unlock|setDeleted.*admin|unlock.*method' src/main/java/com/iemr/common/data/users/User.java --type=java

echo -e "\n=== Verify the User.java has `@Expose` annotation on lockedAt field ==="
rg -n '@Expose.*lockedAt|lockedAt.*@Expose|private.*lockedAt' src/main/java/com/iemr/common/data/users/User.java --type=java -B2 -A2

Repository: PSMRI/Common-API

Length of output: 362


Add endpoint for manual admin account unlock.

The PR exposes account lock status via the @Expose annotation on the lockedAt field in User.java, fulfilling one requirement. However, no admin endpoint exists for manually unlocking accounts—only automatic unlock after 24 hours is implemented in checkUserLoginFailedAttempt().

Add a service method and controller endpoint to allow admins to manually unlock accounts:

Example implementation
// In IEMRAdminUserServiceImpl
public void adminUnlockAccount(Long userId) throws IEMRException {
    User user = iEMRUserRepositoryCustom.findUserByUserID(userId);
    if (user != null) {
        user.setDeleted(false);
        user.setFailedAttempt(0);
        user.setLockedAt(null);
        iEMRUserRepositoryCustom.save(user);
        logger.info("Admin unlocked user account. User ID: {}", userId);
    }
}

// In IEMRAdminController
`@PostMapping`("/unlockUserAccount")
public String unlockUserAccount(`@RequestBody` Map<String, Long> request) {
    Long userId = request.get("userID");
    iemrAdminUserServiceImpl.adminUnlockAccount(userId);
    // Return response
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java`
around lines 304 - 320, Add a manual admin unlock: implement a new service
method adminUnlockAccount(Long userId) in IEMRAdminUserServiceImpl that looks up
the user via iEMRUserRepositoryCustom.findUserByUserID(userId), returns/throws
if not found, sets user.setDeleted(false), user.setFailedAttempt(0),
user.setLockedAt(null), saves with iEMRUserRepositoryCustom.save(user) and logs
via logger.info("Admin unlocked user account. User ID: {}", userId);
declare/throw IEMRException as needed. Expose a controller endpoint in
IEMRAdminController (e.g., `@PostMapping`("/unlockUserAccount")) which accepts a
request containing userID, validates input, calls
iemrAdminUserServiceImpl.adminUnlockAccount(userId), handles exceptions and
returns an appropriate success/failure response. Ensure null checks, proper
exception handling, and permission (admin) guard where applicable.

@sonarqubecloud
Copy link
Copy Markdown

@Aarti-panchal01
Copy link
Copy Markdown
Author

Hi @vishwab1, @vanitha1822 could you please review this PR and let me know if it looks good or if any changes are needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C4GT Community]: Account Lockout After Multiple Failed Login Attempts – Auto Unlock Required After 24 Hours

1 participant