diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..913e1435 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md - Common-API + +## Project Overview + +Common-API is the gateway microservice for the AMRIT healthcare platform. It provides shared APIs consumed by all frontend UIs including authentication, beneficiary registration, call handling, location masters, notifications, feedback, reporting, and integrations with external systems (c-Zentrix CTI, Everwell, eAusadha, eSanjeevani, ABDM, Firebase, Honeywell POCT devices). + +## Tech Stack + +- Java 17 +- Spring Boot 3.2.2 +- Spring Data JPA / Hibernate +- MySQL 8.0 +- Redis (session management, caching) +- MongoDB (optional, for specific integrations) +- Maven (build tool) +- Swagger/OpenAPI (API documentation) +- Lombok, MapStruct +- CryptoJS-compatible AES encryption +- Firebase Admin SDK +- WAR packaging (deploys to Wildfly) + +## Build and Run + +```bash +# Build +mvn clean install -DENV_VAR=local + +# Run locally (start Redis first) +mvn spring-boot:run -DENV_VAR=local + +# Package WAR +mvn -B package --file pom.xml -P # profiles: dev, local, test, ci, uat + +# Run tests +mvn test +``` + +### Configuration + +- Copy `src/main/environment/common_example.properties` to `common_local.properties` and edit. +- Environment selected via `-DENV_VAR=`. +- Swagger UI: `http://localhost:8083/swagger-ui.html` + +## Package Structure + +Base package: `com.iemr.common` + +| Layer | Package | Description | +|-------|---------|-------------| +| Controllers | `controller.*` | REST endpoints (40+ sub-packages) | +| Services | `service.*` | Business logic | +| Repositories | `repository.*`, `repo.*` | JPA repositories | +| Entities | `data.*` | JPA entity classes | +| DTOs | `model.*` | Transfer objects | +| Mappers | `mapper.*` | Object mapping | +| Config | `config.*` | Swagger, encryption, Firebase, Quartz, prototypes | +| Constants | `constant` | Application constants | +| Utils | `utils.*` | Redis, HTTP, session, validation, exception | + +## Key Functional Domains + +- **Authentication/Authorization**: `controller.users` - login, session, user management +- **Beneficiary Registration**: `controller.beneficiary` - create, search, update beneficiaries +- **Call Handling**: `controller.callhandling` - CTI integration, call lifecycle +- **Feedback/Grievance**: `controller.feedback`, `controller.grievance` - feedback and complaint management +- **Location**: `controller.location` - state, district, block, village masters +- **Notifications**: `controller.notification` - alerts, SMS, email, Firebase push +- **Reporting**: `controller.report`, `controller.secondaryReport` - CRM reports +- **Helpline 104**: `controller.helpline104history` - medical advice history +- **COVID**: `controller.covid` - vaccination status +- **CTI Integration**: `controller.cti` - c-Zentrix computer telephony +- **External Integrations**: `controller.eausadha`, `controller.esanjeevani`, `controller.everwell`, `controller.honeywell`, `controller.brd`, `controller.carestream` +- **ABDM**: `controller.abdmfacility` - Ayushman Bharat Digital Mission +- **KM File Management**: `controller.kmfilemanager` - OpenKM document management +- **OTP/SMS**: `controller.otp`, `controller.sms` (via SMS gateway) +- **Scheduling**: `controller.questionconfig`, `controller.scheme` +- **Door-to-Door App**: `controller.door_to_door_app` - field worker support +- **NHM Dashboard**: `controller.nhmdashboard` - National Health Mission integration + +## Architecture Notes + +- Entry point: `CommonMain.java` (main class in `utils` package) +- Acts as the API gateway; all frontend UIs authenticate through Common-API +- Session management via Redis with 27-minute timeout +- HTTP interceptors attach `Authorization` and `ServerAuthorization` headers +- Status code `5002` signals session expiration to frontends +- AES + PBKDF2 encryption for password handling (`config.encryption`) +- Firebase integration for push notifications (`config.firebase`) +- Quartz scheduler for background jobs (`config.quartz`) +- Extensive test coverage with unit tests under `src/test/` + +## CI/CD + +- GitHub Actions: `package.yml`, `build-on-pull-request.yml`, `sast.yml`, `commit-lint.yml`, `codeql.yml` +- Conventional Commits enforced via Husky + commitlint +- Checkstyle configuration in `checkstyle.xml` +- JaCoCo for code coverage, SonarQube integration configured +- Dockerfile for containerized deployment diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index f39faf2f..547e069f 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -3,12 +3,12 @@ spring.datasource.url=@env.DATABASE_URL@ spring.datasource.username=@env.DATABASE_USERNAME@ spring.datasource.password=@env.DATABASE_PASSWORD@ -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=@env.REPORTING_DATABASE_USERNAME@ secondary.datasource.password=@env.REPORTING_DATABASE_PASSWORD@ secondary.datasource.url=@env.REPORTING_DATABASE_URL@ -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=@env.KM_API_BASE_PROTOCOL@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index e3851b54..064090b3 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -3,12 +3,12 @@ spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=${REPORTING_DATABASE_USERNAME} secondary.datasource.password=${REPORTING_DATABASE_PASSWORD} secondary.datasource.url=${REPORTING_DATABASE_URL} -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=${KM_API_BASE_PROTOCOL} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 7ec9c410..b37ab216 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -7,12 +7,12 @@ spring.datasource.password=1234 encDbUserName=zFlYsp9Z0s+lRvLM15A3g/Ba0w8VGs/1usuW7EsGF3k= encDbPass=JGGAGn5wTlrbTLUHY+5BzfBa0w8VGs/1usuW7EsGF3k= -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=root secondary.datasource.password=1234 secondary.datasource.url=jdbc:mysql://localhost:3306/db_reporting -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration @@ -42,6 +42,7 @@ genben-api=http://localhost:8092 send-sms=false sendSMSUrl = http://localhost:8083/sms/sendSMS source-address=AIDSHL +sms-consent-source-address= sms-username= sms-password= send-message-url= 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 349c3b1e..2ae06257 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -79,6 +79,7 @@ @RequestMapping("/user") @RestController public class IEMRAdminController { + private static final String USER_ID_FIELD = "userId"; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); @@ -672,6 +673,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); @@ -1337,4 +1345,104 @@ public ResponseEntity checkUserDetails(@PathVariable("userName") String userN } } + + @Operation(summary = "Lock user account") + @PostMapping(value = "/lockUserAccount", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String lockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean locked = iemrAdminUserServiceImpl.lockUserAccount(userId); + response.setResponse(locked ? "User account successfully locked" : "User account was already locked"); + } catch (Exception e) { + logger.error("Error locking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Unlock user account locked due to failed login attempts") + @PostMapping(value = "/unlockUserAccount", 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") + @PostMapping(value = "/getUserLockStatus", 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(USER_ID_FIELD) || requestObj.get(USER_ID_FIELD).isJsonNull()) { + throw new IEMRException(USER_ID_FIELD + " is required"); + } + JsonElement userIdElement = requestObj.get(USER_ID_FIELD); + if (!userIdElement.isJsonPrimitive() || !userIdElement.getAsJsonPrimitive().isNumber()) { + throw new IEMRException(USER_ID_FIELD + " must be a number"); + } + return userIdElement.getAsLong(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to parse {} from request: {}", USER_ID_FIELD, e.getMessage()); + throw new IEMRException("Invalid request body"); + } + } + + 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) { + logger.error("Authentication failed while extracting user ID: {}", e.getMessage()); + throw new IEMRException("Authentication failed"); + } + } + + 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 4a9c6158..83e5ac27 100644 --- a/src/main/java/com/iemr/common/data/users/User.java +++ b/src/main/java/com/iemr/common/data/users/User.java @@ -216,6 +216,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 cc1abccc..4a2bef76 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); @Query("SELECT u FROM User u WHERE LOWER(u.userName) = LOWER(:userName)") 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 26b7bb15..a366fa0c 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -125,6 +125,12 @@ public List getUserServiceRoleMappingForProvider(Integ List findUserIdByUserName(String userName) throws IEMRException; + boolean lockUserAccount(Long userId) 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 71d72c97..f9624f13 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,121 @@ 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()); + } 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, 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 +345,36 @@ 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 "Your account has been locked. You can try tomorrow or connect to the administrator."; + } + + private long calculateRemainingLockTime(java.sql.Timestamp lockTimestamp) { + long lockTimeMillis = lockTimestamp.getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long unlockTimeMillis = lockTimeMillis + getLockDurationMillis(); + return unlockTimeMillis - currentTimeMillis; } /** @@ -319,67 +383,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 +1215,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,13 +1231,150 @@ public User getUserById(Long userId) throws IEMRException { @Override public List getUserIdbyUserName(String userName) { - return iEMRUserRepositoryCustom.findByUserName(userName); } - @Override + @Override public List findUserIdByUserName(String userName) { - return iEMRUserRepositoryCustom.findUserName(userName); } + + @Override + public boolean lockUserAccount(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()) { + if (user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Activate the account before locking it."); + } + logger.info("User account is already locked for userID: {}", userId); + return false; + } + + user.setDeleted(true); + user.setFailedAttempt(getFailedAttemptThreshold()); + user.setLockTimestamp(new java.sql.Timestamp(System.currentTimeMillis())); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually locked user account for userID: {}", userId); + return true; + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error locking user account with ID: " + userId, e); + throw new IEMRException("Error locking user account: " + e.getMessage(), e); + } + } + + @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); + } + + private static final Set ADMIN_ROLE_NAMES = Set.of("admin", "supervisor", "provideradmin"); + + @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().trim().toLowerCase(); + if (ADMIN_ROLE_NAMES.contains(roleName)) { + return true; + } + } + } + return false; + } catch (Exception e) { + logger.error("Error checking admin privileges for userId: " + userId, e); + return false; + } + } } diff --git a/src/main/java/com/iemr/common/utils/CookieUtil.java b/src/main/java/com/iemr/common/utils/CookieUtil.java index 92c071c5..1562e3c4 100644 --- a/src/main/java/com/iemr/common/utils/CookieUtil.java +++ b/src/main/java/com/iemr/common/utils/CookieUtil.java @@ -35,8 +35,8 @@ public void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, H // Make the cookie HttpOnly to prevent JavaScript access for security cookie.setHttpOnly(true); - // Set the Max-Age (expiry time) in seconds (8 hours) - cookie.setMaxAge(60 * 60 * 8); // 8 hours expiration + // Set the Max-Age (expiry time) in seconds (10 hours) + cookie.setMaxAge(60 * 60 * 10); // 10 hours expiration // Set the path to "/" so the cookie is available across the entire application cookie.setPath("/"); diff --git a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java index 68947e1a..1c8c24b2 100644 --- a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java +++ b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java @@ -48,12 +48,10 @@ import jakarta.annotation.PostConstruct; -import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @Service // @Primary diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index f73e6fd2..74ae2cbb 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -23,6 +23,7 @@ secondary.datasource.username= secondary.datasource.password= secondary.datasource.url=jdbc:h2:mem:reportingdb secondary.datasource.driver-class-name=org.h2.Driver +tempFilePath=temp springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e827b0fc..4a2de342 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -172,6 +172,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