From 3c5b5b895cd4bc6d1bdddfce596ce9f6054ea0e2 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 6 May 2026 12:18:05 +0530 Subject: [PATCH 01/17] add bengali language translation on dynamic forms --- .../iemr/common/data/dynamic_from/FormFieldOption.java | 3 +++ .../com/iemr/common/data/translation/Translation.java | 2 ++ .../service/dynamicForm/FormMasterServiceImpl.java | 9 ++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java index 8cfeb0de..05f06df2 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java @@ -27,6 +27,9 @@ public class FormFieldOption { @Column(name = "label_as") private String labelAs; + @Column(name = "label_bn") + private String labelBn; + @Column(name = "sort_order") private Integer sortOrder; diff --git a/src/main/java/com/iemr/common/data/translation/Translation.java b/src/main/java/com/iemr/common/data/translation/Translation.java index 0dad116d..e0104a53 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -20,6 +20,8 @@ public class Translation { private String hindiTranslation; @Column(name = "assamese_translation") private String assameseTranslation; + @Column(name = "bengali_translation") + private String bengaliTranslation; @Column(name = "is_active") private Boolean isActive; } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java index 0bf1c2fe..38154f4a 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -160,6 +160,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedLabel = label.getEnglish(); + }else if ("bn".equalsIgnoreCase(lang)) { + translatedLabel = label.getBengaliTranslation(); + } } @@ -171,6 +174,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedPlaceHolder = placeHolder.getEnglish(); + } else if ("bn".equalsIgnoreCase(lang)) { + translatedPlaceHolder = placeHolder.getBengaliTranslation(); + } } @@ -203,7 +209,8 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str map.put("value", opt.getValue()); if ("hi".equalsIgnoreCase(lang)) map.put("label", opt.getLabelHi()); else if ("as".equalsIgnoreCase(lang)) map.put("label", opt.getLabelAs()); - else map.put("label", opt.getLabelEn()); + else if("en".equals(lang)) map.put("label", opt.getLabelEn()); + else if("bn".equals(lang)) map.put("label", opt.getLabelBn()); return map; }) .collect(Collectors.toList()); From bc54def0d8c37942baf5b12d65f4ee09a1a3afc0 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Tue, 12 May 2026 13:17:28 +0530 Subject: [PATCH 02/17] change api version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b4e8c0d9..ffe74404 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.8.2 war Common-API From 3e0bbb798eb8b1894858ab7caa438976866fea9b Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Fri, 22 May 2026 19:05:21 +0530 Subject: [PATCH 03/17] Sn/3.8.1 (#423) * fix: aam-2313 phone number leading with zero - removed zero * fix: allow concurrent sessions for admin, superadmin, and supervisor roles Co-Authored-By: Claude Sonnet 4.6 * fix: added admin and superadmin for the condition --------- Co-authored-by: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) 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..5ac134cd 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,6 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -180,11 +181,22 @@ public String userAuthenticate( if (m_User.getUserName() != null && (m_User.getDoLogout() == null || !m_User.getDoLogout()) && (m_User.getWithCredentials() != null && m_User.getWithCredentials())) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + String userRole = ""; + if (mUser.size() == 1 && mUser.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUser.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); @@ -412,16 +424,28 @@ public String logOutUserFromConcurrentSession( deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); - // Denylist the active JWT so System 1's requests are immediately rejected - String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); - if (jtiData != null) { - String[] parts = jtiData.split("\\|", 2); - tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); - if (parts.length > 1) { - redisTemplate.delete("user_" + parts[1]); + String userRole = ""; + if (mUsers.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUsers.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + // Denylist the active JWT so the first system's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + redisTemplate.delete("jti:" + usernameKey); } - stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); From cbb18177013dedbed09d83cddcb0414d2aab162d Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:13:52 +0530 Subject: [PATCH 04/17] fix: use stringRedisTemplate for jti: key read/delete in concurrent session logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redisTemplate has Jackson2JsonRedisSerializer as value serializer, so reading the plain-string jti: value caused a deserialization failure (statusCode 5000). jti: keys are written via stringRedisTemplate at login, so reads and deletes must also use stringRedisTemplate — restoring the behaviour from commit 80fa0e5e that was accidentally reverted in #423. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/common/controller/users/IEMRAdminController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5ac134cd..f9942b5e 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -436,7 +436,7 @@ public String logOutUserFromConcurrentSession( if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { // Denylist the active JWT so the first system's requests are immediately rejected String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); if (jtiData != null) { String[] parts = jtiData.split("\\|", 2); String jti = parts[0]; @@ -444,7 +444,7 @@ public String logOutUserFromConcurrentSession( if (parts.length > 1) { redisTemplate.delete("user_" + parts[1]); } - redisTemplate.delete("jti:" + usernameKey); + stringRedisTemplate.delete("jti:" + usernameKey); } } From 8b548d383988d62fa2354b08f95124fdda643387 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:48:34 +0530 Subject: [PATCH 05/17] fix: correct exempt roles and allow superadmin concurrent sessions - Fix role name from 'admin' to 'provideradmin' to match actual DB value - Add concurrent session exemption to superUserAuthenticate so SuperAdmin can log in from multiple tabs without being blocked Co-Authored-By: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 f9942b5e..f23c1739 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,7 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); - private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -560,11 +560,13 @@ public String superUserAuthenticate( String refreshToken = null; boolean isMobile = false; if (m_User.getUserName() != null && (m_User.getDoLogout() == null || m_User.getDoLogout() == false)) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(m_User.getUserName().trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); From 2c23d9374afc6f3c4d88873a0627fd6b32e80be3 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 29 May 2026 12:25:25 +0530 Subject: [PATCH 06/17] Implement feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 89 ++++++++++++++++++- .../service/users/IEMRAdminUserService.java | 2 +- .../users/IEMRAdminUserServiceImpl.java | 5 ++ 3 files changed, 93 insertions(+), 3 deletions(-) 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 845fe890..ec0298e8 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -171,7 +171,93 @@ public String userAuthenticate( } String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword()); - List mUser = iemrAdminUserServiceImpl.userAuthenticate(m_User.getUserName(), decryptPassword); + // Fetch user + List existingUser = iemrAdminUserServiceImpl.userExitsCheck(m_User.getUserName()); + + /* + * ========================================= + * ACCOUNT LOCK CHECK + * ========================================= + */ + if(!existingUser.isEmpty()){ + if (existingUser.get(0) != null + && existingUser.get(0).getFailedAttempt() != null + && existingUser.get(0).getFailedAttempt() >= 5) { + + throw new IEMRException( + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + } + } + + + List mUser = iemrAdminUserServiceImpl + .userAuthenticate(m_User.getUserName(), decryptPassword); + + /* + * ========================================= + * FAILED LOGIN ATTEMPT LOGIC + * ========================================= + */ + if (mUser == null || mUser.isEmpty()) { + if(!existingUser.isEmpty()){ + if (existingUser != null) { + + Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null + ? existingUser.get(0).getFailedAttempt() + : 0; + + failedAttempt++; + + existingUser.get(0).setFailedAttempt(failedAttempt); + + iemrAdminUserServiceImpl.save(existingUser.get(0)); + + int remainingAttempts = 5 - failedAttempt; + + // Lock account on 5th attempt + if (failedAttempt >= 5) { + + + + response.setError(new IEMRException( + "Your account has been locked due to multiple failed login attempts.")); + return response.toString(); + } + + // Warning on 3rd attempt + if (failedAttempt == 4) { + + + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts + + ". If you enter wrong username or password again, your account will be locked.")); + return response.toString(); + } + + + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts)); + return response.toString(); + + } + } + + + throw new IEMRException("Invalid username or password."); + } + + /* + * ========================================= + * RESET FAILED ATTEMPTS ON SUCCESS LOGIN + * ========================================= + */ + User loggedInUser = mUser.get(0); + + loggedInUser.setFailedAttempt(0); + + iemrAdminUserServiceImpl.save(loggedInUser); JSONObject resMap = new JSONObject(); JSONObject serviceRoleMultiMap = new JSONObject(); JSONObject serviceRoleMap = new JSONObject(); @@ -253,7 +339,6 @@ public String userAuthenticate( // Facility data for ALL users - common pattern, empty if not applicable try { if (mUser.size() == 1) { - User loggedInUser = mUser.get(0); String userRoleName = ""; if (loggedInUser.getM_UserServiceRoleMapping() != null) { for (UserServiceRoleMapping usrm : loggedInUser.getM_UserServiceRoleMapping()) { 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..1db05322 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -126,5 +126,5 @@ public List getUserServiceRoleMappingForProvider(Integ List findUserIdByUserName(String userName) throws IEMRException; - + User save(User loggedInUser); } 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..3d20be1b 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -1230,4 +1230,9 @@ public List findUserIdByUserName(String userName) { return iEMRUserRepositoryCustom.findUserName(userName); } + + @Override + public User save(User loggedInUser) { + return iEMRUserRepositoryCustom.save(loggedInUser); + } } From fe3336a170ac1bf3d2271d26665fc5888b5dbfdc Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 29 May 2026 13:09:15 +0530 Subject: [PATCH 07/17] Implement feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) 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 ec0298e8..b3644743 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -198,54 +198,49 @@ public String userAuthenticate( * FAILED LOGIN ATTEMPT LOGIC * ========================================= */ - if (mUser == null || mUser.isEmpty()) { - if(!existingUser.isEmpty()){ - if (existingUser != null) { - - Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null - ? existingUser.get(0).getFailedAttempt() - : 0; - - failedAttempt++; + if(!existingUser.isEmpty()){ + if (existingUser != null) { - existingUser.get(0).setFailedAttempt(failedAttempt); + Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null + ? existingUser.get(0).getFailedAttempt() + : 0; - iemrAdminUserServiceImpl.save(existingUser.get(0)); + failedAttempt++; - int remainingAttempts = 5 - failedAttempt; + existingUser.get(0).setFailedAttempt(failedAttempt); - // Lock account on 5th attempt - if (failedAttempt >= 5) { + iemrAdminUserServiceImpl.save(existingUser.get(0)); + int remainingAttempts = 5 - failedAttempt; + // Lock account on 5th attempt + if (failedAttempt >= 5) { - response.setError(new IEMRException( - "Your account has been locked due to multiple failed login attempts.")); - return response.toString(); - } - // Warning on 3rd attempt - if (failedAttempt == 4) { + response.setError(new IEMRException( + "Your account has been locked due to multiple failed login attempts.")); + return response.toString(); + } - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts - + ". If you enter wrong username or password again, your account will be locked.")); - return response.toString(); - } + // Warning on 3rd attempt + if (failedAttempt == 4) { - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts)); - return response.toString(); + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts + + ". If you enter wrong username or password again, your account will be locked.")); + return response.toString(); + } - } - } + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts)); + return response.toString(); - throw new IEMRException("Invalid username or password."); + } } /* From a6a6d3c4814db3d45c3d3f3e88bdbb5fd6c1adeb Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 12 Jun 2026 13:56:13 +0530 Subject: [PATCH 08/17] fix: delete camp:vanID and camp:parkingPlaceID from Redis on MMU logout Redis keys were stored on MMU login with 30-day TTL but never deleted on logout. Added cleanup in userLogout() so stale camp config does not persist after the MMU session ends. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/common/controller/users/IEMRAdminController.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 f23c1739..a82374a5 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -1018,6 +1018,13 @@ public String userLogout(HttpServletRequest request) { try { deleteSessionObjectByGettingSessionDetails(request.getHeader("Authorization")); sessionObject.deleteSessionObject(request.getHeader("Authorization")); + try { + stringRedisTemplate.delete("camp:vanID"); + stringRedisTemplate.delete("camp:parkingPlaceID"); + logger.info("Camp config cleared from Redis on MMU logout"); + } catch (Exception redisEx) { + logger.warn("Failed to clear camp Redis keys on logout: {}", redisEx.getMessage()); + } response.setResponse("Success"); } catch (Exception e) { response.setError(e); From 758bf3674694bad1177f15cb5a7f95fb5986c2f1 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 16 Jun 2026 15:11:41 +0530 Subject: [PATCH 09/17] feat: add endpoint to expose server's LAN IP address Lets a device on the same network discover the API's LAN IP and port without needing it typed in manually, and bumps version to 3.8.2. --- pom.xml | 2 +- .../controller/connect/ConnectController.java | 57 +++++++++++++++++++ .../com/iemr/common/utils/NetworkUtil.java | 56 ++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/iemr/common/controller/connect/ConnectController.java create mode 100644 src/main/java/com/iemr/common/utils/NetworkUtil.java diff --git a/pom.xml b/pom.xml index b4e8c0d9..ffe74404 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.8.2 war Common-API diff --git a/src/main/java/com/iemr/common/controller/connect/ConnectController.java b/src/main/java/com/iemr/common/controller/connect/ConnectController.java new file mode 100644 index 00000000..7ecf45ba --- /dev/null +++ b/src/main/java/com/iemr/common/controller/connect/ConnectController.java @@ -0,0 +1,57 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.controller.connect; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.common.utils.NetworkUtil; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Exposes the server's LAN address so a mobile device on the same wifi + * network can connect to this API. + */ +@RestController +@RequestMapping(value = "/public/connect") +public class ConnectController { + + @Value("${server.port:8080}") + private int serverPort; + + @Operation(summary = "Get the server's LAN IP address and port") + @GetMapping(value = "/info", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConnectInfo() { + Map response = new LinkedHashMap<>(); + response.put("ip", NetworkUtil.getLanIPAddress()); + response.put("port", serverPort); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/iemr/common/utils/NetworkUtil.java b/src/main/java/com/iemr/common/utils/NetworkUtil.java new file mode 100644 index 00000000..b5ded4d5 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/NetworkUtil.java @@ -0,0 +1,56 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import java.net.DatagramSocket; +import java.net.InetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves the single LAN-facing IPv4 address the OS would use for outbound + * traffic, used to build a URL that a device on the same wifi network (e.g. + * a mobile phone) can connect to. A machine can have several network + * interfaces (wifi, ethernet, VPN, virtual adapters); connecting a UDP + * socket to an external address - without sending any packet - makes the OS + * routing table pick the one real outbound interface, avoiding ambiguity + * from iterating all interfaces. + */ +public final class NetworkUtil { + + private static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class); + private static final String FALLBACK_IP = "127.0.0.1"; + + private NetworkUtil() { + } + + public static String getLanIPAddress() { + try (DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + return socket.getLocalAddress().getHostAddress(); + } catch (Exception e) { + logger.error("Failed to resolve LAN IP address", e); + return FALLBACK_IP; + } + } +} From be92d7c5cbbd993b569213194eb691e803ef91d0 Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Tue, 16 Jun 2026 17:14:44 +0530 Subject: [PATCH 10/17] fix: extented the token expiry timing --- src/main/java/com/iemr/common/utils/CookieUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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("/"); From e04714b8c97315440bde52955a8a7ecfd8211ec8 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 15:26:27 +0530 Subject: [PATCH 11/17] fix issue of feature multiple login attempt --- .../users/IEMRAdminUserServiceImpl.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 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 3d20be1b..13adda14 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,12 +265,28 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); + int currentFailedAttempt = + user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; + + int newFailedAttempt = currentFailedAttempt + 1; + int remainingAttempts = failedAttempt - newFailedAttempt; + if (newFailedAttempt < failedAttempt) { + + user.setFailedAttempt(newFailedAttempt); user = iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { + + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + + throw new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts); + }else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); user = iEMRUserRepositoryCustom.save(user); @@ -278,14 +294,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. 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."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } else { checkUserAccountStatus(user); From 818257c3535b4dff2c26aa0a4af200bec635b0fd Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 16:44:01 +0530 Subject: [PATCH 12/17] fix issue of feature multiple login attempt --- .../users/IEMRAdminUserServiceImpl.java | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 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 13adda14..3d20be1b 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,28 +265,12 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - int currentFailedAttempt = - user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; - - int newFailedAttempt = currentFailedAttempt + 1; - int remainingAttempts = failedAttempt - newFailedAttempt; - if (newFailedAttempt < failedAttempt) { - - user.setFailedAttempt(newFailedAttempt); + if (user.getFailedAttempt() + 1 < failedAttempt) { + user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - - if (remainingAttempts == 1) { - throw new IEMRException( - "Invalid username or password. Remaining attempts: 1. " - + "If you enter wrong username or password again, your account will be locked."); - } - - throw new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts); - }else if (user.getFailedAttempt() + 1 >= failedAttempt) { + 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); @@ -294,14 +278,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + "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( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + "Invalid username or password. Please contact administrator."); } } else { checkUserAccountStatus(user); From a27db85fa8bf9c5324245b756fff31cd5ea4ec61 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 17:07:04 +0530 Subject: [PATCH 13/17] fix issue of feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 71 ------------------- .../users/IEMRAdminUserServiceImpl.java | 28 ++++++-- 2 files changed, 22 insertions(+), 77 deletions(-) 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 b3644743..f3612fdb 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -171,83 +171,12 @@ public String userAuthenticate( } String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword()); - // Fetch user - List existingUser = iemrAdminUserServiceImpl.userExitsCheck(m_User.getUserName()); - - /* - * ========================================= - * ACCOUNT LOCK CHECK - * ========================================= - */ - if(!existingUser.isEmpty()){ - if (existingUser.get(0) != null - && existingUser.get(0).getFailedAttempt() != null - && existingUser.get(0).getFailedAttempt() >= 5) { - - throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); - } - } List mUser = iemrAdminUserServiceImpl .userAuthenticate(m_User.getUserName(), decryptPassword); - /* - * ========================================= - * FAILED LOGIN ATTEMPT LOGIC - * ========================================= - */ - if(!existingUser.isEmpty()){ - if (existingUser != null) { - - Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null - ? existingUser.get(0).getFailedAttempt() - : 0; - - failedAttempt++; - - existingUser.get(0).setFailedAttempt(failedAttempt); - - iemrAdminUserServiceImpl.save(existingUser.get(0)); - - int remainingAttempts = 5 - failedAttempt; - - // Lock account on 5th attempt - if (failedAttempt >= 5) { - - - - response.setError(new IEMRException( - "Your account has been locked due to multiple failed login attempts.")); - return response.toString(); - } - - // Warning on 3rd attempt - if (failedAttempt == 4) { - - - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts - + ". If you enter wrong username or password again, your account will be locked.")); - return response.toString(); - } - - - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts)); - return response.toString(); - - } - } - /* - * ========================================= - * RESET FAILED ATTEMPTS ON SUCCESS LOGIN - * ========================================= - */ User loggedInUser = mUser.get(0); loggedInUser.setFailedAttempt(0); 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 3d20be1b..13adda14 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,12 +265,28 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); + int currentFailedAttempt = + user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; + + int newFailedAttempt = currentFailedAttempt + 1; + int remainingAttempts = failedAttempt - newFailedAttempt; + if (newFailedAttempt < failedAttempt) { + + user.setFailedAttempt(newFailedAttempt); user = iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { + + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + + throw new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts); + }else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); user = iEMRUserRepositoryCustom.save(user); @@ -278,14 +294,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. 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."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } else { checkUserAccountStatus(user); From c3013e0a0b46c08369ee7ca6595673df9a1b79b4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:11:36 +0530 Subject: [PATCH 14/17] Increase timing of refresh token (#435) --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a2de342..bc7a8c14 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -177,7 +177,7 @@ account.lock.duration.hours=24 #Jwt Token configuration jwt.access.expiration=28800000 -jwt.refresh.expiration=604800000 +jwt.refresh.expiration=7776000000 # local env From 16dc5af4306f3f1bfc52041943fd810323a8ff54 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 26 Jun 2026 15:29:24 +0530 Subject: [PATCH 15/17] fix: restore remaining-attempts lockout message from PR #431 Saurav's PR #431 added a "Remaining attempts: N" message on failed login to warn users before account lockout. Port this into the refactored handleFailedLoginAttempt helper so both userAuthenticate and superUserAuthenticate keep the behavior after merging release-3.8.1's account-lock refactor into release-3.8.2. --- .../common/service/users/IEMRAdminUserServiceImpl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 d8d4abe8..6b5dabf6 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -308,7 +308,13 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr user.setFailedAttempt(currentAttempts + 1); iEMRUserRepositoryCustom.save(user); logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); + int remainingAttempts = failedAttemptThreshold - (currentAttempts + 1); + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + throw new IEMRException("Invalid username or password. Remaining attempts: " + remainingAttempts); } else { java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); user.setFailedAttempt(currentAttempts + 1); From 865c266dca92ad24829421c46bd2c1d494cc59ac Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 26 Jun 2026 16:04:16 +0530 Subject: [PATCH 16/17] fix: restore account-lockout message from PR #432 The merge of release-3.8.1's account-lock refactor into release-3.8.2 replaced Saurav Mishra's inline multiple-login-attempt logic (PR #426/#431/#432) with the refactored handlePasswordValidationAndLocking helper. The refactor kept the "Remaining attempts" warning but dropped the final lockout message text, falling back to the unrelated generateLockoutErrorMessage used for already-locked accounts. Restore the exact lockout message from PR #432 so the refactored helper preserves both branches' intended behavior. --- .../iemr/common/service/users/IEMRAdminUserServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 6b5dabf6..e411b9ef 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -323,7 +323,8 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr iEMRUserRepositoryCustom.save(user); logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", failedAttemptThreshold); - throw new IEMRException(generateLockoutErrorMessage(lockTime)); + throw new IEMRException( + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } From d2efa54b304f77ba23757a3abdd0e795c8d0372c Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:04:10 +0530 Subject: [PATCH 17/17] fix: AMM-2343 on lock/Unlock the api was returning 500 error (#437) Co-authored-by: Sneha --- .../java/com/iemr/common/utils/JwtUserIdValidationFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 557d5da5..3be36bbf 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -168,7 +168,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from cookie"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromCookie)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } @@ -176,7 +176,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from header"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromHeader)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; }