Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3c5b5b8
add bengali language translation on dynamic forms
SauravBizbRolly May 6, 2026
eb9de55
Merge pull request #406 from PSMRI/feature/add_bengali_language
SauravBizbRolly May 6, 2026
2cd890c
Merge remote-tracking branch 'upstream/vbb/release-3.8.1' into sm/rel…
SauravBizbRolly May 12, 2026
bc54def
change api version
SauravBizbRolly May 12, 2026
3e0bbb7
Sn/3.8.1 (#423)
snehar-nd May 22, 2026
cbb1817
fix: use stringRedisTemplate for jti: key read/delete in concurrent s…
vishwab1 May 22, 2026
8b548d3
fix: correct exempt roles and allow superadmin concurrent sessions
vishwab1 May 22, 2026
a8102b1
Merge pull request #424 from PSMRI/vb/fixSession
snehar-nd May 22, 2026
2c23d93
Implement feature multiple login attempt
SauravBizbRolly May 29, 2026
fe3336a
Implement feature multiple login attempt
SauravBizbRolly May 29, 2026
5424887
Merge pull request #426 from PSMRI/feature/handel_multiple_attempt_pa…
SauravBizbRolly Jun 4, 2026
a6a6d3c
fix: delete camp:vanID and camp:parkingPlaceID from Redis on MMU logout
vishwab1 Jun 12, 2026
758bf36
feat: add endpoint to expose server's LAN IP address
vishwab1 Jun 16, 2026
be92d7c
fix: extented the token expiry timing
snehar-nd Jun 16, 2026
fc1deba
Merge pull request #428 from PSMRI/sn/tokenExpiry
snehar-nd Jun 16, 2026
e04714b
fix issue of feature multiple login attempt
SauravBizbRolly Jun 22, 2026
fee73b3
Merge pull request #431 from PSMRI/fix_multiple_login_issue
SauravBizbRolly Jun 22, 2026
818257c
fix issue of feature multiple login attempt
SauravBizbRolly Jun 22, 2026
a27db85
fix issue of feature multiple login attempt
SauravBizbRolly Jun 22, 2026
38e4c02
Merge pull request #432 from PSMRI/fix_login_issue_multiple_login
SauravBizbRolly Jun 22, 2026
73caab7
Merge remote-tracking branch 'origin/main' into vb/release-3.8.2
vishwab1 Jun 25, 2026
d08403f
Merge remote-tracking branch 'origin/release-3.8.1' into vb/release-3…
vishwab1 Jun 25, 2026
5a34772
Merge remote-tracking branch 'origin/release-3.8.2' into vb/release-3…
vishwab1 Jun 25, 2026
c9a2694
Merge pull request #434 from PSMRI/vb/release-3.8.2
snehar-nd Jun 25, 2026
c3013e0
Increase timing of refresh token (#435)
SauravBizbRolly Jun 26, 2026
16dc5af
fix: restore remaining-attempts lockout message from PR #431
vishwab1 Jun 26, 2026
8743f34
Merge remote-tracking branch 'origin/release-3.8.2' into vb/release-3…
vishwab1 Jun 26, 2026
865c266
fix: restore account-lockout message from PR #432
vishwab1 Jun 26, 2026
89d1da2
Merge pull request #436 from PSMRI/vb/release-3.8.2
SauravBizbRolly Jun 26, 2026
d2efa54
fix: AMM-2343 on lock/Unlock the api was returning 500 error (#437)
snehar-nd Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.iemr.common-API</groupId>
<artifactId>common-api</artifactId>
<version>3.8.1</version>
<version>3.8.2</version>
<packaging>war</packaging>

<name>Common-API</name>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object>> getConnectInfo() {
Map<String, Object> response = new LinkedHashMap<>();
response.put("ip", NetworkUtil.getLanIPAddress());
response.put("port", serverPort);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
private static final String USER_ID_FIELD = "userId";
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
private InputMapper inputMapper = new InputMapper();
private static final Set<String> CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin");

// @Value("${captcha.enable-captcha}")
private boolean enableCaptcha =false;
Expand Down Expand Up @@ -172,7 +173,17 @@
}

String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword());
List<User> mUser = iemrAdminUserServiceImpl.userAuthenticate(m_User.getUserName(), decryptPassword);


List<User> mUser = iemrAdminUserServiceImpl
.userAuthenticate(m_User.getUserName(), decryptPassword);


User loggedInUser = mUser.get(0);

loggedInUser.setFailedAttempt(0);

iemrAdminUserServiceImpl.save(loggedInUser);
JSONObject resMap = new JSONObject();
JSONObject serviceRoleMultiMap = new JSONObject();
JSONObject serviceRoleMap = new JSONObject();
Expand All @@ -181,11 +192,22 @@
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());
Expand Down Expand Up @@ -254,7 +276,6 @@
// 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()) {
Expand Down Expand Up @@ -413,16 +434,28 @@
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 = stringRedisTemplate.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]);
}
stringRedisTemplate.delete("jti:" + usernameKey);
}
stringRedisTemplate.delete("jti:" + usernameKey);
}

response.setResponse("User successfully logged out");
Expand Down Expand Up @@ -537,11 +570,13 @@
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());
Expand Down Expand Up @@ -1000,6 +1035,13 @@
try {
deleteSessionObjectByGettingSessionDetails(request.getHeader("Authorization"));
sessionObject.deleteSessionObject(request.getHeader("Authorization"));
try {

Check warning on line 1038 in src/main/java/com/iemr/common/controller/users/IEMRAdminController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested try block into a separate method.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Common-API&issues=AZ8YVvgU-_kpGiYTbzWU&open=AZ8YVvgU-_kpGiYTbzWU&pullRequest=438
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

}
}

Expand All @@ -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();

}
}

Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ public List<ServiceRoleScreenMapping> getUserServiceRoleMappingForProvider(Integ

boolean hasAdminPrivileges(Long userId) throws IEMRException;

User save(User loggedInUser);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -317,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.");
}
}

Expand Down Expand Up @@ -1377,4 +1384,9 @@ public boolean hasAdminPrivileges(Long userId) throws IEMRException {
return false;
}
}

@Override
public User save(User loggedInUser) {
return iEMRUserRepositoryCustom.save(loggedInUser);
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/iemr/common/utils/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("/");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ 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;
}
} else if (jwtFromHeader != null) {
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;
}
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/com/iemr/common/utils/NetworkUtil.java
Original file line number Diff line number Diff line change
@@ -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);

Check warning on line 49 in src/main/java/com/iemr/common/utils/NetworkUtil.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure using this hardcoded IP address is safe here.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Common-API&issues=AZ8YVvjq-_kpGiYTbzWV&open=AZ8YVvjq-_kpGiYTbzWV&pullRequest=438

Check notice

Code scanning / SonarCloud

IP addresses should not be hardcoded Low

Make sure using this hardcoded IP address is safe here. See more on SonarQube Cloud
return socket.getLocalAddress().getHostAddress();
} catch (Exception e) {
logger.error("Failed to resolve LAN IP address", e);
return FALLBACK_IP;
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading