From 5a28f4b1082b3cfd552d2d34bdfec1dba011827b Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:00:42 +0530 Subject: [PATCH 01/70] Update application.properties --- 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 18723465..a1135a2a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -373,4 +373,4 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent - +cors.allowed-origin = From ea12e3e16f68484da66a07f621e2fdccd0ca96d9 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 26 Nov 2025 19:54:09 +0530 Subject: [PATCH 02/70] add column in create BeneficiaryModel --- .../com/iemr/common/model/beneficiary/BeneficiaryModel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java index e7a7a3de..5d42f275 100644 --- a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java +++ b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java @@ -118,7 +118,10 @@ public class BeneficiaryModel implements Comparable { private Boolean isMarried; @Expose - private Integer doYouHavechildren; + private boolean doYouHavechildren; + + @Expose + private Integer noOfchildren; @Expose private Integer noofAlivechildren; From 50c595288043614744079694c17d02749d4f1d5b Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:44:57 +0530 Subject: [PATCH 03/70] Elasticsearch implementation for Beneficiary Search (#324) * fix: implement functionality to search beneficiaries with Elasticsearch * fix: remove unwanted import * fix: update pom.xml * fix: change the response code --- pom.xml | 2 +- .../BeneficiaryRegistrationController.java | 58 +++++++++++++ .../beneficiary/IEMRSearchUserService.java | 2 + .../IEMRSearchUserServiceImpl.java | 31 ++++++- .../IdentityBeneficiaryService.java | 5 ++ .../IdentityBeneficiaryServiceImpl.java | 82 ++++++++++++++++++- src/main/resources/application.properties | 1 + 7 files changed, 178 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 11ad9f37..171ab162 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.0 + 3.6.1 war Common-API diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 8f573d6d..cdce13b0 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -70,6 +71,8 @@ import com.iemr.common.service.userbeneficiarydata.MaritalStatusService; import com.iemr.common.service.userbeneficiarydata.StatusService; import com.iemr.common.service.userbeneficiarydata.TitleService; +import com.iemr.common.utils.CookieUtil; +import com.iemr.common.utils.JwtUtil; import com.iemr.common.utils.mapper.InputMapper; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -104,6 +107,10 @@ public class BeneficiaryRegistrationController { private GovtIdentityTypeService govtIdentityTypeService; + + @Autowired + private JwtUtil jwtUtil; + @Autowired public void setBenRelationshipTypeService(BenRelationshipTypeService benRelationshipTypeService) { this.benRelationshipTypeService = benRelationshipTypeService; @@ -342,6 +349,57 @@ public String searchUserByPhone( return response.toString(); } + @Operation(summary = "Provide the list of beneficiaries using Elasticsearch") + @RequestMapping(value = "/searchUser", method = RequestMethod.POST, headers = "Authorization") + public String searchUser( + @RequestBody String request, + @RequestHeader(value = "Authorization", required = false) String auth, HttpServletRequest httpRequest) { + + OutputResponse response = new OutputResponse(); + + try { + logger.info("Universal search request received"); + + JsonParser parser = new JsonParser(); + JsonObject requestObj = parser.parse(request).getAsJsonObject(); + + String searchQuery = null; + if (requestObj.has("search") && !requestObj.get("search").isJsonNull()) { + searchQuery = requestObj.get("search").getAsString(); + } + + if (searchQuery == null || searchQuery.trim().isEmpty()) { + response.setError(400, "Search query is required"); + return response.toString(); + } + + String jwtToken = CookieUtil.getJwtTokenFromCookie(httpRequest); + String userId = jwtUtil.getUserIdFromToken(jwtToken); + int userID=Integer.parseInt(userId); + + Boolean is1097 = false; + if (requestObj.has("is1097") && !requestObj.get("is1097").isJsonNull()) { + is1097 = requestObj.get("is1097").getAsBoolean(); + } + + logger.info("Searching with query: {}, userId: {}, is1097: {}", searchQuery, userID, is1097); + + String result = iemrSearchUserService.searchUser(searchQuery, userID, auth, is1097); + + if (result == null || result.trim().isEmpty()) { + response.setError(200, "No beneficiaries found"); + return response.toString(); + } + + return result; + + } catch (Exception e) { + logger.error("Error in universal search: {}", e.getMessage(), e); + response.setError(400, "Error searching beneficiaries: " + e.getMessage()); + return response.toString(); + } + } + @Operation(summary = "Provide the list of beneficiaries based on search criteria") @RequestMapping(value = "/searchBeneficiary", method = RequestMethod.POST, headers = "Authorization") public String searchBeneficiary( diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java index 6e7848cd..ab4c40fd 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java @@ -38,6 +38,8 @@ String findByBeneficiaryPhoneNo(BenPhoneMap benPhoneMap, Integer pageNo, Integer String findBeneficiary(BeneficiaryModel request, String auth) throws Exception; + String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception; + List userExitsCheckWithId(String beneficiaryID, String auth, Boolean is1097) throws Exception; public List userExitsCheckWithHealthId_ABHAId(String healthID, String auth, Boolean is1097) diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java index f67d7815..7c9bfc2d 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java @@ -25,6 +25,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import org.slf4j.Logger; @@ -76,7 +77,7 @@ import com.iemr.common.repository.userbeneficiarydata.MaritalStatusRepository; import com.iemr.common.repository.userbeneficiarydata.SexualOrientationRepository; import com.iemr.common.repository.userbeneficiarydata.TitleRepository; -import com.iemr.common.utils.mapper.OutputMapper; +import com.iemr.common.utils.exception.IEMRException; /** * @@ -322,6 +323,34 @@ private void setBeneficiaryGender(List iBeneficiary) { } + /** + Universal search using Elasticsearch + */ + @Override + public String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception { + + try { + if (searchQuery == null || searchQuery.trim().isEmpty()) { + throw new IEMRException("Search query is required"); + } + + logger.info("Universal search with query: {}, userId: {}", searchQuery, userId); + + Map response = + identityBeneficiaryService.searchBeneficiariesUsingES( + searchQuery, userId, auth, is1097 + ); + + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(response); + + } catch (Exception e) { + logger.error("Error in universal search", e); + throw new Exception("Error searching beneficiaries: " + e.getMessage(), e); + } +} + + // Advance search @Override public String findBeneficiary(BeneficiaryModel i_beneficiary, String auth) throws Exception { diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java index 8b84bc8a..5ebe3b24 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -69,4 +70,8 @@ public List getBeneficiaryListByFamilyId(String familyId, Stri public List getBeneficiaryListByGovId(String identity, String auth, Boolean is1097) throws IEMRException; + + public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097) throws IEMRException; + + } diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java index f9ca6c96..0136c0f8 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java @@ -21,15 +21,20 @@ */ package com.iemr.common.service.beneficiary; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import com.google.gson.*; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.iemr.common.dto.identity.BeneficiariesDTO; @@ -43,6 +48,12 @@ import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; +import org.springframework.beans.factory.annotation.Value; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + @Service public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryService { @@ -107,6 +118,75 @@ public List getBeneficiaryListByIDs(HashSet benIdList, String return listBenDetailForOutboundDTO; } + /** + Call Identity API's Elasticsearch universal search + */ + @Override + public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097 ) throws IEMRException { + + Map response = new HashMap<>(); + + try { + HashMap headers = new HashMap<>(); + if (auth != null && !auth.isEmpty()) { + headers.put("Authorization", auth); + } + + String baseUrl = ConfigProperties + .getPropertyByName("identity-api-url-searchByES") + .replace( + IDENTITY_BASE_URL, + (Boolean.TRUE.equals(is1097)) ? identity1097BaseURL : identityBaseURL + ); + + StringBuilder url = new StringBuilder(baseUrl) + .append("?query=").append(URLEncoder.encode(query, StandardCharsets.UTF_8)); + + if (userId != null) { + url.append("&userId=").append(userId); + } + + logger.info("Calling Identity ES search URL: {}", url); + + String result = httpUtils.get(url.toString(), headers); + + if (result == null || result.isEmpty()) { + response.put("data", Collections.emptyList()); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + return response; + } + + ObjectMapper mapper = new ObjectMapper(); + + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + + JsonNode rootNode = mapper.readTree(result); + + if (rootNode.has("statusCode") && rootNode.get("statusCode").asInt() != 200) { + String errMsg = rootNode.has("errorMessage") + ? rootNode.get("errorMessage").asText() + : "Identity ES search failed"; + throw new IEMRException(errMsg); + } + + response.put("data", rootNode.path("data")); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + + return response; + + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error calling Identity ES search API", e); + throw new IEMRException("Error calling Identity ES search API"); + } +} + @Override public List getPartialBeneficiaryListByIDs(HashSet benIdList, String auth, Boolean is1097) throws IEMRException { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18723465..8ab4669b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -118,6 +118,7 @@ identity-api-url-getByBenRegId =IDENTITY_BASE_URL/id/getByBenRegId?benRegId= identity-api-url-benCreate =IDENTITY_BASE_URL/id/create identity-api-url-benEdit =IDENTITY_BASE_URL/id/edit identity-api-url-benEditEducationCommunity=IDENTITY_BASE_URL/id/editEducationOrCommunity +identity-api-url-searchByES=IDENTITY_BASE_URL/beneficiary/search identity-api-url-getByFamilyId=IDENTITY_BASE_URL/id/searchByFamilyId?familyId= identity-api-url-getByGovIdentity=IDENTITY_BASE_URL/id/searchByGovIdentity?identity= From 1bc9298bf8783f400a022507782d4c5d91ddef34 Mon Sep 17 00:00:00 2001 From: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Date: Wed, 31 Dec 2025 16:08:54 +0530 Subject: [PATCH 04/70] variable added --- src/main/environment/common_ci.properties | 1 + src/main/environment/common_docker.properties | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 0184b32f..9f54a35d 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -19,6 +19,7 @@ km-base-path=@env.KM_API_BASE_PATH@ km-root-path=/okm:personal/users/ km-guest-user=@env.KM_GUEST_USER@ km-guest-password=@env.KM_GUEST_PASSWORD@ +tempFilePath=@env.TEMP_FILE_PATH@ # CTI Config cti-server-ip=@env.CTI_SERVER_IP@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index a81ea62e..59cb580d 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -126,7 +126,7 @@ everwellRegisterBenficiary = ${COMMON_API_BASE_URL}/beneficiary/create ## LungAssessment credentials lungAssessmentEmail = ${SWAASA_EMAIL} lungAssessmentPassword =${SWAASA_PASSWORD} - +tempFilePath=${TEMP_FILE_PATH} ## SWASSA APIs lungAssessmentAdminLogin = ${SWAASA_BASE_URL}/api/adminLogin From c501e8037f967fa497d132a9780b8913452927e6 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 7 Jan 2026 18:41:15 +0530 Subject: [PATCH 05/70] update language --- .../java/com/iemr/common/data/translation/Translation.java | 2 ++ .../common/service/dynamicForm/FormMasterServiceImpl.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 81a906fa..0dad116d 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -18,6 +18,8 @@ public class Translation { private String english; @Column(name = "hindi_translation") private String hindiTranslation; + @Column(name = "assamese_translation") + private String assameseTranslation; @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 df019de7..cfe643b3 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -122,8 +122,11 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang) { if (t != null) { if ("hi".equalsIgnoreCase(lang)) { translatedLabel = t.getHindiTranslation(); - } else { + } else if("am".equalsIgnoreCase(lang)){ + translatedLabel = t.getAssameseTranslation(); + }else if("en".equalsIgnoreCase(lang)){ translatedLabel = t.getEnglish(); + } } From ec3aac3557f4fec6c5bedcbada3195f17654b42a Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 7 Jan 2026 18:50:37 +0530 Subject: [PATCH 06/70] update language --- .../iemr/common/service/dynamicForm/FormMasterServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cfe643b3..98d93dbe 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -122,7 +122,7 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang) { if (t != null) { if ("hi".equalsIgnoreCase(lang)) { translatedLabel = t.getHindiTranslation(); - } else if("am".equalsIgnoreCase(lang)){ + } else if("as".equalsIgnoreCase(lang)){ translatedLabel = t.getAssameseTranslation(); }else if("en".equalsIgnoreCase(lang)){ translatedLabel = t.getEnglish(); From bafc8794a9afd5750b26ff74d61c3d1a7862d959 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:15:36 +0530 Subject: [PATCH 07/70] Downgrade version from 3.6.1 to 3.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 171ab162..11ad9f37 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.1 + 3.6.0 war Common-API From 65efdc7362f558e6cadc8f735c7606cdec42fb28 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:26:07 +0530 Subject: [PATCH 08/70] Elastic Search Implementation for Advanced Search (#327) * fix: cherry-pick commits for advanced search * fix: cherry-pick commit for token issue - mobile application * fix: add the missing properties * fix: add function to retrieve userid * fix: move the fetch Userid to jwtUtil --- .../BeneficiaryRegistrationController.java | 74 ++++-- .../beneficiary/IEMRSearchUserService.java | 2 + .../IEMRSearchUserServiceImpl.java | 104 +++++++-- .../IdentityBeneficiaryService.java | 2 + .../IdentityBeneficiaryServiceImpl.java | 214 +++++++++++------- .../java/com/iemr/common/utils/JwtUtil.java | 55 +++++ .../iemr/common/utils/RestTemplateUtil.java | 7 +- .../utils/http/HTTPRequestInterceptor.java | 177 ++++++++------- src/main/resources/application.properties | 1 + 9 files changed, 423 insertions(+), 213 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index cdce13b0..3784f3a9 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -106,9 +106,7 @@ public class BeneficiaryRegistrationController { private BeneficiaryOccupationService beneficiaryOccupationService; private GovtIdentityTypeService govtIdentityTypeService; - - - @Autowired + @Autowired private JwtUtil jwtUtil; @Autowired @@ -351,54 +349,51 @@ public String searchUserByPhone( @Operation(summary = "Provide the list of beneficiaries using Elasticsearch") @RequestMapping(value = "/searchUser", method = RequestMethod.POST, headers = "Authorization") - public String searchUser( - @RequestBody String request, - @RequestHeader(value = "Authorization", required = false) String auth, HttpServletRequest httpRequest) { - + public String searchUser(@RequestBody String request, HttpServletRequest httpRequest) { OutputResponse response = new OutputResponse(); - try { logger.info("Universal search request received"); - + JsonParser parser = new JsonParser(); JsonObject requestObj = parser.parse(request).getAsJsonObject(); - + String searchQuery = null; if (requestObj.has("search") && !requestObj.get("search").isJsonNull()) { searchQuery = requestObj.get("search").getAsString(); } - + if (searchQuery == null || searchQuery.trim().isEmpty()) { response.setError(400, "Search query is required"); return response.toString(); } - - String jwtToken = CookieUtil.getJwtTokenFromCookie(httpRequest); - String userId = jwtUtil.getUserIdFromToken(jwtToken); - int userID=Integer.parseInt(userId); - + + String auth = httpRequest.getHeader("Authorization"); + + Integer userID = jwtUtil.getUserIdFromRequest(httpRequest); + + logger.info("ES search for userId: {}", userID); + Boolean is1097 = false; if (requestObj.has("is1097") && !requestObj.get("is1097").isJsonNull()) { is1097 = requestObj.get("is1097").getAsBoolean(); } - + logger.info("Searching with query: {}, userId: {}, is1097: {}", searchQuery, userID, is1097); - String result = iemrSearchUserService.searchUser(searchQuery, userID, auth, is1097); - + if (result == null || result.trim().isEmpty()) { response.setError(200, "No beneficiaries found"); return response.toString(); } - + return result; - + } catch (Exception e) { logger.error("Error in universal search: {}", e.getMessage(), e); response.setError(400, "Error searching beneficiaries: " + e.getMessage()); return response.toString(); } - } + } @Operation(summary = "Provide the list of beneficiaries based on search criteria") @RequestMapping(value = "/searchBeneficiary", method = RequestMethod.POST, headers = "Authorization") @@ -422,6 +417,41 @@ public String searchBeneficiary( return output.toString(); } + /** + * Elasticsearch-based advanced search endpoint + */ + @Operation(summary = "Advanced search beneficiaries using Elasticsearch") + @RequestMapping(value = "/searchBeneficiaryES", method = RequestMethod.POST, headers = "Authorization") + public String searchBeneficiaryES( + @RequestBody BeneficiaryModel request, + HttpServletRequest httpRequest) { + + logger.info("searchBeneficiaryES request: {}", request); + OutputResponse output = new OutputResponse(); + + try { + + String auth = httpRequest.getHeader("Authorization"); + + Integer userID = jwtUtil.getUserIdFromRequest(httpRequest); + + logger.info("ES Advanced search for userId: {}", userID); + + String result = iemrSearchUserService.findBeneficiaryES(request, userID, auth); + + return result; + + } catch (NumberFormatException ne) { + logger.error("searchBeneficiaryES failed with number format error: {}", ne.getMessage(), ne); + output.setError(400, "Invalid number format in search criteria"); + return output.toString(); + } catch (Exception e) { + logger.error("searchBeneficiaryES failed with error: {}", e.getMessage(), e); + output.setError(500, "Error searching beneficiaries: " + e.getMessage()); + return output.toString(); + } + } + @Operation(summary = "Provide all common data list needed for beneficiary registration") @RequestMapping(value = "/getRegistrationData", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") public String getRegistrationData() { diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java index ab4c40fd..e39cfcab 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java @@ -40,6 +40,8 @@ String findByBeneficiaryPhoneNo(BenPhoneMap benPhoneMap, Integer pageNo, Integer String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception; + String findBeneficiaryES(BeneficiaryModel i_beneficiary, Integer userId, String auth) throws Exception; + List userExitsCheckWithId(String beneficiaryID, String auth, Boolean is1097) throws Exception; public List userExitsCheckWithHealthId_ABHAId(String healthID, String auth, Boolean is1097) diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java index 7c9bfc2d..28d664c8 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java @@ -199,7 +199,7 @@ private void addCreatedDateToOtherFields(BeneficiaryModel beneficiaryModel) { JsonNode otherFieldsNode = objectMapper.readTree(beneficiaryModel.getOtherFields()); // Convert createdDate to a string - String createdDateString = beneficiaryModel.getCreatedDate().toString(); + String createdDateString = beneficiaryModel.getCreatedDate().toString(); // Add createdDate to the JSON node ((ObjectNode) otherFieldsNode).put("createdDate", createdDateString); @@ -220,10 +220,10 @@ public List userExitsCheckWithHealthId_ABHAId(String healthID, List beneficiaryList = new ArrayList(); // search patient by ben id, call Identity API List listBen = null; - if(healthID.contains("@")) { + if (healthID.contains("@")) { listBen = identityBeneficiaryService.getBeneficiaryListByHealthID_ABHAAddress(healthID, auth, is1097); - }else { + } else { String healthIdNumber = getHealthId(healthID); listBen = identityBeneficiaryService.getBeneficiaryListByHealthIDNo_ABHAIDNo(healthIdNumber, auth, is1097); } @@ -233,6 +233,7 @@ public List userExitsCheckWithHealthId_ABHAId(String healthID, } return beneficiaryList; } + private String getHealthId(String healthID) { String healthIdNumber = null; if (null != healthID) { @@ -250,6 +251,7 @@ private String getHealthId(String healthID) { } return healthIdNumber; } + // search patient by healthidNo / ABHA Id No @Override public List userExitsCheckWithHealthIdNo_ABHAIdNo(String healthIDNo, String auth, Boolean is1097) @@ -324,32 +326,88 @@ private void setBeneficiaryGender(List iBeneficiary) { } /** - Universal search using Elasticsearch - */ + * Universal search using Elasticsearch + */ @Override public String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception { - try { - if (searchQuery == null || searchQuery.trim().isEmpty()) { - throw new IEMRException("Search query is required"); - } + try { + if (searchQuery == null || searchQuery.trim().isEmpty()) { + throw new IEMRException("Search query is required"); + } + + logger.info("Universal search with query: {}, userId: {}", searchQuery, userId); - logger.info("Universal search with query: {}, userId: {}", searchQuery, userId); + Map response = identityBeneficiaryService.searchBeneficiariesUsingES( + searchQuery, userId, auth, is1097); - Map response = - identityBeneficiaryService.searchBeneficiariesUsingES( - searchQuery, userId, auth, is1097 - ); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(response); - ObjectMapper mapper = new ObjectMapper(); - return mapper.writeValueAsString(response); + } catch (Exception e) { + logger.error("Error in universal search", e); + throw new Exception("Error searching beneficiaries: " + e.getMessage(), e); + } + } - } catch (Exception e) { - logger.error("Error in universal search", e); - throw new Exception("Error searching beneficiaries: " + e.getMessage(), e); - } -} + /** + * Advanced search using Elasticsearch with multiple criteria + */ + + @Override + public String findBeneficiaryES( + BeneficiaryModel i_beneficiary, + Integer userId, + String auth) throws Exception { + + try { + IdentitySearchDTO identitySearchDTO = identityBenEditMapper.getidentitysearchModel(i_beneficiary); + + if (i_beneficiary.getDOB() != null) { + identitySearchDTO.setDob(i_beneficiary.getDOB()); + } + + if (i_beneficiary.getHouseHoldID() != null) { + identitySearchDTO.setHouseHoldID(i_beneficiary.getHouseHoldID()); + } + + if (i_beneficiary.getIsD2D() != null) { + identitySearchDTO.setIsD2D(i_beneficiary.getIsD2D()); + } + + if (i_beneficiary.getBenPhoneMaps() != null + && !i_beneficiary.getBenPhoneMaps().isEmpty()) { + identitySearchDTO.setContactNumber( + i_beneficiary.getBenPhoneMaps().get(0).getPhoneNo()); + } + + if (i_beneficiary.getBeneficiaryID() != null + && !i_beneficiary.getBeneficiaryID().isEmpty()) { + identitySearchDTO.setBeneficiaryId( + new BigInteger(i_beneficiary.getBeneficiaryID())); + } + + i_beneficiary.setIs1097(Boolean.TRUE.equals(i_beneficiary.getIs1097())); + Gson gson = new GsonBuilder() + .setDateFormat("yyyy-MM-dd") + .create(); + + String requestJson = gson.toJson(identitySearchDTO); + + Map response = identityBeneficiaryService.searchBeneficiaryListES( + requestJson, + auth, + i_beneficiary.getIs1097()); + + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(response); + + } catch (Exception e) { + logger.error("Error in ES advance search", e); + throw new Exception("Error searching beneficiaries using ES", e); + } + } // Advance search @Override @@ -393,7 +451,7 @@ public String findBeneficiary(BeneficiaryModel i_beneficiary, String auth) throw + (beneficiaryList != null ? beneficiaryList.size() : "No Beneficiary Found")); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(beneficiaryList); - + } // get response mapper @@ -403,7 +461,7 @@ public List getBeneficiaryListFromMapper(List { BeneficiaryModel beneficiary = benCompleteMapper.benDetailForOutboundDTOToIBeneficiary(beneficiaryModel); - if(null != beneficiaryModel && null != beneficiaryModel.getBeneficiaryDetails()) { + if (null != beneficiaryModel && null != beneficiaryModel.getBeneficiaryDetails()) { beneficiary.setCommunityName(beneficiaryModel.getBeneficiaryDetails().getCommunity()); beneficiary.setReligion(beneficiaryModel.getBeneficiaryDetails().getReligion()); beneficiary.setReligionName(beneficiaryModel.getBeneficiaryDetails().getReligion()); diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java index 5ebe3b24..41a132b0 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java @@ -54,6 +54,8 @@ List getBeneficiaryListByBenRegID(Long benRegId, String auth, List searchBeneficiaryList(String identitySearchDTO, String auth, Boolean is1097) throws IEMRException; + public Map searchBeneficiaryListES(String identitySearchDTO, String auth, Boolean is1097) throws IEMRException ; + Integer editIdentityEditDTOCommunityorEducation(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException; diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java index 0136c0f8..350f2527 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java @@ -34,7 +34,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.iemr.common.dto.identity.BeneficiariesDTO; @@ -70,12 +69,10 @@ public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryServic private static final String IDENTITY_BASE_URL = "IDENTITY_BASE_URL"; @Value("${genben-api}") - private String BEN_GEN ; - + private String BEN_GEN; @Value("${generateBeneficiaryIDs-api-url}") - private String BEN_GEN_API_URL ; - + private String BEN_GEN_API_URL; @Override // public List getBeneficiaryListByIDs() {// search by regID @@ -99,13 +96,10 @@ public List getBeneficiaryListByIDs(HashSet benIdList, String } if (null != result) { JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -118,20 +112,21 @@ public List getBeneficiaryListByIDs(HashSet benIdList, String return listBenDetailForOutboundDTO; } - /** - Call Identity API's Elasticsearch universal search - */ + /** + * Call Identity API's Elasticsearch universal search + */ @Override - public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097 ) throws IEMRException { + public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097) + throws IEMRException { - Map response = new HashMap<>(); + Map response = new HashMap<>(); try { HashMap headers = new HashMap<>(); if (auth != null && !auth.isEmpty()) { headers.put("Authorization", auth); } - + String baseUrl = ConfigProperties .getPropertyByName("identity-api-url-searchByES") .replace( @@ -139,58 +134,57 @@ public Map searchBeneficiariesUsingES(String query, Integer user (Boolean.TRUE.equals(is1097)) ? identity1097BaseURL : identityBaseURL ); - StringBuilder url = new StringBuilder(baseUrl) - .append("?query=").append(URLEncoder.encode(query, StandardCharsets.UTF_8)); + StringBuilder url = new StringBuilder(baseUrl) + .append("?query=").append(URLEncoder.encode(query, StandardCharsets.UTF_8)); - if (userId != null) { - url.append("&userId=").append(userId); - } + if (userId != null) { + url.append("&userId=").append(userId); + } - logger.info("Calling Identity ES search URL: {}", url); + logger.info("Calling Identity ES search URL: {}", url); - String result = httpUtils.get(url.toString(), headers); + String result = httpUtils.get(url.toString()); - if (result == null || result.isEmpty()) { - response.put("data", Collections.emptyList()); - response.put("statusCode", 200); - response.put("status", "Success"); - response.put("errorMessage", "Success"); - return response; - } + if (result == null || result.isEmpty()) { + response.put("data", Collections.emptyList()); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + return response; + } - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); - JsonNode rootNode = mapper.readTree(result); + JsonNode rootNode = mapper.readTree(result); - if (rootNode.has("statusCode") && rootNode.get("statusCode").asInt() != 200) { - String errMsg = rootNode.has("errorMessage") - ? rootNode.get("errorMessage").asText() - : "Identity ES search failed"; - throw new IEMRException(errMsg); - } + if (rootNode.has("statusCode") && rootNode.get("statusCode").asInt() != 200) { + String errMsg = rootNode.has("errorMessage") + ? rootNode.get("errorMessage").asText() + : "Identity ES search failed"; + throw new IEMRException(errMsg); + } - response.put("data", rootNode.path("data")); - response.put("statusCode", 200); - response.put("status", "Success"); - response.put("errorMessage", "Success"); + response.put("data", rootNode.path("data")); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); - return response; + return response; - } catch (IEMRException e) { - throw e; - } catch (Exception e) { - logger.error("Error calling Identity ES search API", e); - throw new IEMRException("Error calling Identity ES search API"); - } -} + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error calling Identity ES search API", e); + throw new IEMRException("Error calling Identity ES search API"); + } + } @Override public List getPartialBeneficiaryListByIDs(HashSet benIdList, String auth, Boolean is1097) throws IEMRException { - // TODO Auto-generated method stub List listBenDetailForOutboundDTO = new ArrayList<>(); JsonParser parser = new JsonParser(); @@ -210,13 +204,10 @@ public List getPartialBeneficiaryListByIDs(HashSet benI throw new IEMRException(identityResponse.getErrorMessage()); } JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -231,9 +222,9 @@ public List getPartialBeneficiaryListByIDs(HashSet benI // search beneficiaries by phone number public List getBeneficiaryListByPhone(String phoneNo, String auth, Boolean is1097) throws IEMRException { - logger.info("Phone no from getBeneficiaryListByPhone: " + phoneNo); - String cleanedPhoneNo = cleanPhoneNumber(phoneNo); - logger.info("Cleaned phone no: " + cleanedPhoneNo); + logger.info("Phone no from getBeneficiaryListByPhone: " + phoneNo); + String cleanedPhoneNo = cleanPhoneNumber(phoneNo); + logger.info("Cleaned phone no: " + cleanedPhoneNo); List listBenDetailForOutboundDTO = new ArrayList<>(); @@ -245,12 +236,13 @@ public List getBeneficiaryListByPhone(String phoneNo, String a if (auth != null) { header.put("Authorization", auth); } - - logger.info("Result="+(ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") + + logger.info("Result=" + (ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo); result = httpUtils.post((ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") - .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo, "", header); + .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo, "", + header); OutputResponse identityResponse = InputMapper.gson().fromJson(result, OutputResponse.class); if (identityResponse.getStatusCode() == OutputResponse.USERID_FAILURE) { @@ -271,22 +263,22 @@ public List getBeneficiaryListByPhone(String phoneNo, String a } private String cleanPhoneNumber(String phoneNumber) { - if (phoneNumber == null || phoneNumber.trim().isEmpty()) { - return phoneNumber; - } - - String cleaned = phoneNumber.trim(); - - // Remove +91 prefix - if (cleaned.startsWith("+91")) { - cleaned = cleaned.substring(3); - } - // Remove 91 prefix if it's a 12-digit number (91 + 10 digit mobile) - else if (cleaned.startsWith("91") && cleaned.length() == 12) { - cleaned = cleaned.substring(2); - } - - return cleaned.trim(); + if (phoneNumber == null || phoneNumber.trim().isEmpty()) { + return phoneNumber; + } + + String cleaned = phoneNumber.trim(); + + // Remove +91 prefix + if (cleaned.startsWith("+91")) { + cleaned = cleaned.substring(3); + } + // Remove 91 prefix if it's a 12-digit number (91 + 10 digit mobile) + else if (cleaned.startsWith("91") && cleaned.length() == 12) { + cleaned = cleaned.substring(2); + } + + return cleaned.trim(); } @Override @@ -533,7 +525,6 @@ public String getIdentityResponse(String request, String auth, Boolean is1097) t return result; } - public Integer editIdentityEditDTO(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException { JsonParser parser = new JsonParser(); @@ -578,13 +569,10 @@ public List searchBeneficiaryList(String identitySearchDTO, St IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL)), identitySearchDTO, header); JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -596,6 +584,68 @@ public List searchBeneficiaryList(String identitySearchDTO, St return listBenDetailForOutboundDTO; } + @Override + public Map searchBeneficiaryListES(String identitySearchDTO, String auth, Boolean is1097) + throws IEMRException { + + Map response = new HashMap<>(); + + try { + HashMap headers = new HashMap<>(); + if (auth != null && !auth.isEmpty()) { + headers.put("Authorization", auth); + } + + String url = ConfigProperties + .getPropertyByName("identity-api-url-advancesearch-es") + .replace( + IDENTITY_BASE_URL, + Boolean.TRUE.equals(is1097) + ? identity1097BaseURL + : identityBaseURL); + + logger.info("Calling Identity ES Advance Search API"); + + String result = httpUtils.post(url, identitySearchDTO, headers); + + if (result == null || result.isEmpty()) { + response.put("data", Collections.emptyList()); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + return response; + } + + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + JsonNode rootNode = mapper.readTree(result); + + if (rootNode.has("statusCode") + && rootNode.get("statusCode").asInt() != 200) { + + String errMsg = rootNode.has("errorMessage") + ? rootNode.get("errorMessage").asText() + : "Identity ES advance search failed"; + + throw new IEMRException(errMsg); + } + + response.put("data", rootNode.path("data")); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + + return response; + + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error calling Identity ES advance search API", e); + throw new IEMRException("Error calling Identity ES advance search API", e); + } + } + @Override public Integer editIdentityEditDTOCommunityorEducation(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException { @@ -635,11 +685,11 @@ public List generateBeneficiaryIDs(String request, String a if (auth != null) { header.put("Authorization", auth); } - + logger.info("Request to generate ben IDs: " + request); logger.info("Generating ben IDs API URL: " + BEN_GEN + BEN_GEN_API_URL); result = httpUtils.post(BEN_GEN + BEN_GEN_API_URL, request, header); -logger.info("Response from generate ben IDs: " + result); + logger.info("Response from generate ben IDs: " + result); OutputResponse identityResponse = inputMapper.gson().fromJson(result, OutputResponse.class); if (identityResponse.getStatusCode() == OutputResponse.USERID_FAILURE) { diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index d8414968..5d37a990 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -2,6 +2,7 @@ import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -161,4 +162,58 @@ public String getUserIdFromToken(String token) { public long getRefreshTokenExpiration() { return REFRESH_EXPIRATION_TIME; } + + /** + * Extract user ID from JWT token in the request (checks header and cookie) + * @param request the HTTP request + * @return the user ID, or null if not found + */ +public Integer getUserIdFromRequest(HttpServletRequest request) { + try { + String jwtToken = request.getHeader("Jwttoken"); + String cookieToken = CookieUtil.getJwtTokenFromCookie(request); + + // Prefer header token, fallback to cookie + String token = (jwtToken != null && !jwtToken.isEmpty()) ? jwtToken : cookieToken; + + if (token == null || token.isEmpty()) { + return null; + } + + Claims claims = validateToken(token); + if (claims == null) { + return null; + } + + String userId = claims.get("userId", String.class); + return userId != null ? Integer.parseInt(userId) : null; + + } catch (Exception e) { + return null; + } +} + +/** + * Extract username from JWT token in the request (checks header and cookie) + * @param request the HTTP request + * @return the username, or null if not found + */ +public String getUsernameFromRequest(HttpServletRequest request) { + try { + String jwtToken = request.getHeader("Jwttoken"); + String cookieToken = CookieUtil.getJwtTokenFromCookie(request); + + String token = (jwtToken != null && !jwtToken.isEmpty()) ? jwtToken : cookieToken; + + if (token == null || token.isEmpty()) { + return null; + } + + Claims claims = validateToken(token); + return claims != null ? claims.getSubject() : null; + + } catch (Exception e) { + return null; + } +} } diff --git a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java index c8299fe7..4e4fa483 100644 --- a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java +++ b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java @@ -39,6 +39,8 @@ public static HttpEntity createRequestEntity(Object body, String authori headers.add(HttpHeaders.AUTHORIZATION, authorization); if (null != requestHeader.getHeader(Constants.JWT_TOKEN)) { headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); + headers.add(HttpHeaders.COOKIE, "Jwttoken=" + requestHeader.getHeader(Constants.JWT_TOKEN)); + } if (null != jwtTokenFromCookie) { headers.add(HttpHeaders.COOKIE, "Jwttoken=" + jwtTokenFromCookie); @@ -77,9 +79,10 @@ public static void getJwttokenFromHeaders(HttpHeaders headers) { if (null != jwtTokenFromCookie) { headers.add(HttpHeaders.COOKIE, Constants.JWT_TOKEN + "=" + jwtTokenFromCookie); } else if (null != requestHeader.getHeader(Constants.JWT_TOKEN)) { - headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); - } + headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); + headers.add(HttpHeaders.COOKIE, Constants.JWT_TOKEN + "=" + requestHeader.getHeader(Constants.JWT_TOKEN)); + } } } diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index 0c609839..b4aaad60 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -36,10 +36,14 @@ import com.iemr.common.utils.sessionobject.SessionObject; import com.iemr.common.utils.validator.Validator; +import com.iemr.common.utils.JwtUtil; +import io.jsonwebtoken.Claims; +import com.iemr.common.utils.CookieUtil; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + @Configuration @Component public class HTTPRequestInterceptor implements HandlerInterceptor { @@ -50,6 +54,9 @@ public class HTTPRequestInterceptor implements HandlerInterceptor { @Value("${cors.allowed-origins}") private String allowedOrigins; + @Autowired + private JwtUtil jwtUtil; + @Autowired public void setValidator(Validator validator) { this.validator = validator; @@ -67,100 +74,101 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons boolean status = true; logger.info("In info preHandle we are Intercepting the Request"); logger.debug("In preHandle we are Intercepting the Request"); - // String authorization = request.getHeader("Authorization"); + // String authorization = request.getHeader("Authorization"); String authorization = null; String preAuth = request.getHeader("Authorization"); - if(null != preAuth && preAuth.contains("Bearer ")) - authorization=preAuth.replace("Bearer ", ""); + if (null != preAuth && preAuth.contains("Bearer ")) + authorization = preAuth.replace("Bearer ", ""); else authorization = preAuth; - + if (authorization == null || authorization.isEmpty()) { - logger.info("Authorization header is null or empty. Skipping HTTPRequestInterceptor."); - return true; // Allow the request to proceed without validation - } - logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization + logger.info("Authorization header is null or empty. Skipping HTTPRequestInterceptor."); + return true; // Allow the request to proceed without validation + } + + logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization + " || method :: " + request.getMethod()); if (!request.getMethod().equalsIgnoreCase("OPTIONS")) { try { String[] requestURIParts = request.getRequestURI().split("/"); String requestAPI = requestURIParts[requestURIParts.length - 1]; switch (requestAPI) { - case "userAuthenticate": - case "superUserAuthenticate": - case "userAuthenticateNew": - case "userAuthenticateV1": - case "forgetPassword": - case "setForgetPassword": - case "changePassword": - case "saveUserSecurityQuesAns": - case "doAgentLogout": - case "userLogout": - case "swagger-ui.html": - case "index.html": - case "index.css": - case "swagger-initializer.js": - case "swagger-config": - case "swagger-ui-bundle.js": - case "swagger-ui.css": - case "ui": - case "swagger-ui-standalone-preset.js": - case "favicon-32x32.png": - case "favicon-16x16.png": - case "swagger-resources": - case "api-docs": - case "updateBenCallIdsInPhoneBlock": - case "userAuthenticateByEncryption": - case "sendOTP": - case "validateOTP": - case "resendOTP": - case "validateSecurityQuestionAndAnswer": - case "logOutUserFromConcurrentSession": - case "refreshToken": - break; - case "error": - status = false; - break; - default: - String remoteAddress = request.getHeader("X-FORWARDED-FOR"); - if (remoteAddress == null || remoteAddress.trim().length() == 0) { - remoteAddress = request.getRemoteAddr(); - } - validator.checkKeyExists(authorization, remoteAddress); - break; + case "userAuthenticate": + case "superUserAuthenticate": + case "userAuthenticateNew": + case "userAuthenticateV1": + case "forgetPassword": + case "setForgetPassword": + case "changePassword": + case "saveUserSecurityQuesAns": + case "doAgentLogout": + case "userLogout": + case "swagger-ui.html": + case "index.html": + case "index.css": + case "swagger-initializer.js": + case "swagger-config": + case "swagger-ui-bundle.js": + case "swagger-ui.css": + case "ui": + case "swagger-ui-standalone-preset.js": + case "favicon-32x32.png": + case "favicon-16x16.png": + case "swagger-resources": + case "api-docs": + case "updateBenCallIdsInPhoneBlock": + case "userAuthenticateByEncryption": + case "sendOTP": + case "validateOTP": + case "resendOTP": + case "validateSecurityQuestionAndAnswer": + case "logOutUserFromConcurrentSession": + case "refreshToken": + break; + case "error": + status = false; + break; + default: + String remoteAddress = request.getHeader("X-FORWARDED-FOR"); + if (remoteAddress == null || remoteAddress.trim().length() == 0) { + remoteAddress = request.getRemoteAddr(); + } + validator.checkKeyExists(authorization, remoteAddress); + break; } } catch (Exception e) { logger.error("Authorization failed: {}", e.getMessage(), e); - String errorMessage = e.getMessage(); - if (errorMessage == null || errorMessage.trim().isEmpty()) { - errorMessage = "Unauthorized access or session expired."; - } - - String jsonErrorResponse = "{" - + "\"status\": \"Unauthorized\"," - + "\"statusCode\": 401," - + "\"errorMessage\": \"" + errorMessage.replace("\"", "\\\"") + "\"" - + "}"; - - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 - response.setContentType(MediaType.APPLICATION_JSON); - - String origin = request.getHeader("Origin"); - if (origin != null && isOriginAllowed(origin)) { - response.setHeader("Access-Control-Allow-Origin", origin); - response.setHeader("Access-Control-Allow-Credentials", "true"); - } else if (origin != null) { - logger.warn("CORS headers NOT added for error response | Unauthorized origin: {}", origin); - } - - // Better to use getBytes().length for accurate byte size - byte[] responseBytes = jsonErrorResponse.getBytes(StandardCharsets.UTF_8); - response.setContentLength(responseBytes.length); - - ServletOutputStream out = response.getOutputStream(); - out.write(responseBytes); - out.flush(); + String errorMessage = e.getMessage(); + if (errorMessage == null || errorMessage.trim().isEmpty()) { + errorMessage = "Unauthorized access or session expired."; + } + + String jsonErrorResponse = "{" + + "\"status\": \"Unauthorized\"," + + "\"statusCode\": 401," + + "\"errorMessage\": \"" + errorMessage.replace("\"", "\\\"") + "\"" + + "}"; + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 + response.setContentType(MediaType.APPLICATION_JSON); + + String origin = request.getHeader("Origin"); + if (origin != null && isOriginAllowed(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } else if (origin != null) { + logger.warn("CORS headers NOT added for error response | Unauthorized origin: {}", origin); + } + + // Better to use getBytes().length for accurate byte size + byte[] responseBytes = jsonErrorResponse.getBytes(StandardCharsets.UTF_8); + response.setContentLength(responseBytes.length); + + ServletOutputStream out = response.getOutputStream(); + out.write(responseBytes); + out.flush(); status = false; } } @@ -172,15 +180,14 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response, throws Exception { try { logger.debug("In postHandle we are Intercepting the Request"); - // String authorization = request.getHeader("Authorization"); String authorization = null; String postAuth = request.getHeader("Authorization"); - if(null != postAuth && postAuth.contains("Bearer ")) - authorization=postAuth.replace("Bearer ", ""); + if (null != postAuth && postAuth.contains("Bearer ")) + authorization = postAuth.replace("Bearer ", ""); else authorization = postAuth; logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization); - + if (authorization != null && !authorization.equals("")) { sessionObject.updateSessionObject(authorization, sessionObject.getSessionObject(authorization)); } @@ -212,8 +219,10 @@ private boolean isOriginAllowed(String origin) { .anyMatch(pattern -> { String regex = pattern .replace(".", "\\.") - .replace("*", ".*"); + .replace("*", ".*"); return origin.matches(regex); }); } + + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8ab4669b..5263ca41 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -110,6 +110,7 @@ iemr.extend.expiry.time.changePassword=true iemr.session.expiry.time.changePassword=600 identity-api-url-advancesearch =IDENTITY_BASE_URL/id/advanceSearch +identity-api-url-advancesearch-es =IDENTITY_BASE_URL/beneficiary/advancedSearchES identity-api-url-getByBenRegIdList =IDENTITY_BASE_URL/id/getByBenRegIdList identity-api-url-getByPartialBenRegIdList =IDENTITY_BASE_URL/id/getByPartialBenRegIdList identity-api-url-getByPhoneNum =IDENTITY_BASE_URL/id/getByPhoneNum?phoneNum= From 38a4147f1a07c607888cbf9953b1703cbd0c90a4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:02:34 +0530 Subject: [PATCH 09/70] Remove empty line in application.properties --- src/main/resources/application.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a1135a2a..57652e66 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -372,5 +372,3 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent - -cors.allowed-origin = From 17620d329ba6d2085e2c49aeeecb6e42bb02d428 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 9 Jan 2026 10:08:42 +0530 Subject: [PATCH 10/70] fix:signature check for mmu --- .../common/service/users/EmployeeSignatureServiceImpl.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java b/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java index 115970ed..eff5c8e4 100644 --- a/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java @@ -33,8 +33,6 @@ public class EmployeeSignatureServiceImpl implements EmployeeSignatureService { @Autowired EmployeeSignatureRepo employeeSignatureRepo; - - @Override public EmployeeSignature fetchSignature(Long userSignID) { // TODO Auto-generated method stub @@ -44,12 +42,12 @@ public EmployeeSignature fetchSignature(Long userSignID) { @Override public EmployeeSignature fetchActiveSignature(Long userSignID) { // New method - fetches only non-deleted records - return employeeSignatureRepo.findOneByUserIDAndDeleted(userSignID, false); + return employeeSignatureRepo.findOneByUserID(userSignID); } public Boolean existSignature(Long userID) { // TODO Auto-generated method stub - return employeeSignatureRepo.countByUserIDAndSignatureNotNull(userID)>0; + return employeeSignatureRepo.countByUserIDAndSignatureNotNull(userID) > 0; } } From 9c1493a3b45de27c946ff29b1a31ac9480f3ca0d Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:24:25 +0530 Subject: [PATCH 11/70] Update application.properties --- src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 57652e66..f34f770c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -372,3 +372,4 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent +cors.allowed-origin = From 984d533671f5e84e58b85c7d5efe0248bebc12e4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:43:02 +0530 Subject: [PATCH 12/70] Update application.properties --- src/main/resources/application.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f34f770c..4409afd4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -372,4 +372,5 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent -cors.allowed-origin = +cors.allowed-origin=https://amritdemo.piramalswasthya.org + From c3bc3cbda47b0465df3008ad3407bc73b6dd7590 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 13 Jan 2026 17:16:25 +0530 Subject: [PATCH 13/70] fix: retrive any user without deleted --- .../controller/users/IEMRAdminController.java | 20 ++++++++++++++++++- .../users/IEMRUserRepositoryCustom.java | 3 +++ .../service/users/IEMRAdminUserService.java | 2 ++ .../users/IEMRAdminUserServiceImpl.java | 6 ++++++ 4 files changed, 30 insertions(+), 1 deletion(-) 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 8bc0e74d..554500f3 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -1224,7 +1224,25 @@ public ResponseEntity getUserDetails(@PathVariable("userName") String userNam return new ResponseEntity<>(Map.of("error", "UserName Not Found"), HttpStatus.NOT_FOUND); } User user = users.get(0); - return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), HttpStatus.OK); + return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), + HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(Map.of("error", "Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR); + } + + } + + @Operation(summary = "Get UserId based on userName") + @GetMapping(value = "/checkUserName/{userName}", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public ResponseEntity checkUserDetails(@PathVariable("userName") String userName) { + try { + List users = iemrAdminUserServiceImpl.findUserIdByUserName(userName); + if (users.isEmpty()) { + return new ResponseEntity<>(Map.of("error", "UserName Not Found"), HttpStatus.NOT_FOUND); + } + User user = users.get(0); + return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), + HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(Map.of("error", "Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR); } 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 3ee48ab3..cc1abccc 100644 --- a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java @@ -78,4 +78,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID, User findByUserID(Long userID); + @Query("SELECT u FROM User u WHERE LOWER(u.userName) = LOWER(:userName)") + List findUserName(@Param("userName") String 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 d7dc6e2e..26b7bb15 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -123,6 +123,8 @@ public List getUserServiceRoleMappingForProvider(Integ List getUserIdbyUserName(String userName) throws IEMRException; + List findUserIdByUserName(String userName) 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 44bd2247..71d72c97 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -1224,4 +1224,10 @@ public List getUserIdbyUserName(String userName) { return iEMRUserRepositoryCustom.findByUserName(userName); } + + @Override + public List findUserIdByUserName(String userName) { + + return iEMRUserRepositoryCustom.findUserName(userName); + } } From 519073753e456648ccf9be428b98fb0037e8c37c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 14 Jan 2026 16:35:19 +0530 Subject: [PATCH 14/70] implement state wise hide un hide form fields --- .../dynamicForm/DynamicFormController.java | 4 +- .../common/data/dynamic_from/FormField.java | 6 + .../common/data/users/UserServiceRole.java | 368 ++++++++++++++++++ .../dto/dynamicForm/FieldResponseDTO.java | 2 + .../repository/users/UserServiceRoleRepo.java | 16 + .../dynamicForm/FormMasterService.java | 2 +- .../dynamicForm/FormMasterServiceImpl.java | 188 +++++---- 7 files changed, 503 insertions(+), 83 deletions(-) create mode 100644 src/main/java/com/iemr/common/data/users/UserServiceRole.java create mode 100644 src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index 30a1bc3f..cd83246f 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -84,9 +84,9 @@ public ResponseEntity> deleteField(@PathVariable Long fieldId) { } @GetMapping(value = "form/{formId}/fields") - public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang) { + public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang,@RequestHeader(value = "JwtToken") String token) { try { - Object result = formMasterService.getStructuredFormByFormId(formId,lang); + Object result = formMasterService.getStructuredFormByFormId(formId,lang,token); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success("Form structure fetched successfully", HttpStatus.OK.value(), result)); } catch (Exception e) { diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java index 39785ae9..e902acf1 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java @@ -53,6 +53,12 @@ public class FormField { @Column(name = "sequence") private Integer sequence; + @Column(name = "is_editable") + private Boolean isEditable; + + @Column(name = "state_code") + private Integer stateCode; + @Column(name = "created_at") private LocalDateTime createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/iemr/common/data/users/UserServiceRole.java b/src/main/java/com/iemr/common/data/users/UserServiceRole.java new file mode 100644 index 00000000..63af40dd --- /dev/null +++ b/src/main/java/com/iemr/common/data/users/UserServiceRole.java @@ -0,0 +1,368 @@ +package com.iemr.common.data.users; + +import jakarta.persistence.*; +import java.util.Objects; + +@Entity +@Table(name = "v_userservicerolemapping", schema = "db_iemr") +public class UserServiceRole { + private Integer userId; + private int usrMappingId; + private String name; + private String userName; + private Short serviceId; + private String serviceName; + private Boolean isNational; + private Integer stateId; + private String stateName; + private Integer workingDistrictId; + private String workingDistrictName; + private Integer workingLocationId; + private Short serviceProviderId; + private String locationName; + private String workingLocationAddress; + private Integer roleId; + private String roleName; + private Integer providerServiceMapId; + private String agentId; + private Short psmStatusId; + private String psmStatus; + private Boolean userServciceRoleDeleted; + private Boolean userDeleted; + private Boolean serviceProviderDeleted; + private Boolean roleDeleted; + private Boolean providerServiceMappingDeleted; + private Boolean isInbound; + private Boolean isOutbound; + private Integer blockid; + private String blockname; + private String villageid; + private String villagename; + + @Basic + @Column(name = "UserID") + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + @Basic + @Column(name = "USRMappingID") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public int getUsrMappingId() { + return usrMappingId; + } + + public void setUsrMappingId(int usrMappingId) { + this.usrMappingId = usrMappingId; + } + + @Basic + @Column(name = "Name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Basic + @Column(name = "UserName") + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @Basic + @Column(name = "ServiceID") + public Short getServiceId() { + return serviceId; + } + + public void setServiceId(Short serviceId) { + this.serviceId = serviceId; + } + + @Basic + @Column(name = "ServiceName") + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + @Basic + @Column(name = "IsNational") + public Boolean getNational() { + return isNational; + } + + public void setNational(Boolean national) { + isNational = national; + } + + @Basic + @Column(name = "StateID") + public Integer getStateId() { + return stateId; + } + + public void setStateId(Integer stateId) { + this.stateId = stateId; + } + + @Basic + @Column(name = "StateName") + public String getStateName() { + return stateName; + } + + public void setStateName(String stateName) { + this.stateName = stateName; + } + + @Basic + @Column(name = "WorkingDistrictID") + public Integer getWorkingDistrictId() { + return workingDistrictId; + } + + public void setWorkingDistrictId(Integer workingDistrictId) { + this.workingDistrictId = workingDistrictId; + } + + @Basic + @Column(name = "WorkingDistrictName") + public String getWorkingDistrictName() { + return workingDistrictName; + } + + public void setWorkingDistrictName(String workingDistrictName) { + this.workingDistrictName = workingDistrictName; + } + + @Basic + @Column(name = "WorkingLocationID") + public Integer getWorkingLocationId() { + return workingLocationId; + } + + public void setWorkingLocationId(Integer workingLocationId) { + this.workingLocationId = workingLocationId; + } + + @Basic + @Column(name = "ServiceProviderID") + public Short getServiceProviderId() { + return serviceProviderId; + } + + public void setServiceProviderId(Short serviceProviderId) { + this.serviceProviderId = serviceProviderId; + } + + @Basic + @Column(name = "LocationName") + public String getLocationName() { + return locationName; + } + + public void setLocationName(String locationName) { + this.locationName = locationName; + } + + @Basic + @Column(name = "WorkingLocationAddress") + public String getWorkingLocationAddress() { + return workingLocationAddress; + } + + public void setWorkingLocationAddress(String workingLocationAddress) { + this.workingLocationAddress = workingLocationAddress; + } + + @Basic + @Column(name = "RoleID") + public Integer getRoleId() { + return roleId; + } + + public void setRoleId(Integer roleId) { + this.roleId = roleId; + } + + @Basic + @Column(name = "RoleName") + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + @Basic + @Column(name = "ProviderServiceMapID") + public Integer getProviderServiceMapId() { + return providerServiceMapId; + } + + public void setProviderServiceMapId(Integer providerServiceMapId) { + this.providerServiceMapId = providerServiceMapId; + } + + @Basic + @Column(name = "AgentID") + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + @Basic + @Column(name = "PSMStatusID") + public Short getPsmStatusId() { + return psmStatusId; + } + + public void setPsmStatusId(Short psmStatusId) { + this.psmStatusId = psmStatusId; + } + + @Basic + @Column(name = "PSMStatus") + public String getPsmStatus() { + return psmStatus; + } + + public void setPsmStatus(String psmStatus) { + this.psmStatus = psmStatus; + } + + @Basic + @Column(name = "UserServciceRoleDeleted") + public Boolean getUserServciceRoleDeleted() { + return userServciceRoleDeleted; + } + + public void setUserServciceRoleDeleted(Boolean userServciceRoleDeleted) { + this.userServciceRoleDeleted = userServciceRoleDeleted; + } + + @Basic + @Column(name = "UserDeleted") + public Boolean getUserDeleted() { + return userDeleted; + } + + public void setUserDeleted(Boolean userDeleted) { + this.userDeleted = userDeleted; + } + + @Basic + @Column(name = "ServiceProviderDeleted") + public Boolean getServiceProviderDeleted() { + return serviceProviderDeleted; + } + + public void setServiceProviderDeleted(Boolean serviceProviderDeleted) { + this.serviceProviderDeleted = serviceProviderDeleted; + } + + @Basic + @Column(name = "RoleDeleted") + public Boolean getRoleDeleted() { + return roleDeleted; + } + + public void setRoleDeleted(Boolean roleDeleted) { + this.roleDeleted = roleDeleted; + } + + @Basic + @Column(name = "ProviderServiceMappingDeleted") + public Boolean getProviderServiceMappingDeleted() { + return providerServiceMappingDeleted; + } + + public void setProviderServiceMappingDeleted(Boolean providerServiceMappingDeleted) { + this.providerServiceMappingDeleted = providerServiceMappingDeleted; + } + + @Basic + @Column(name = "isInbound") + public Boolean getInbound() { + return isInbound; + } + + public void setInbound(Boolean inbound) { + isInbound = inbound; + } + + @Basic + @Column(name = "isOutbound") + public Boolean getOutbound() { + return isOutbound; + } + + public void setOutbound(Boolean outbound) { + isOutbound = outbound; + } + + @Basic + @Column(name = "blockid") + public Integer getBlockid() { + return blockid; + } + + public void setBlockid(Integer blockid) { + this.blockid = blockid; + } + + @Basic + @Column(name = "blockname") + public String getBlockname() { + return blockname; + } + + public void setBlockname(String blockname) { + this.blockname = blockname; + } + + @Basic + @Column(name = "villageid") + public String getVillageid() { + return villageid; + } + + public void setVillageid(String villageid) { + this.villageid = villageid; + } + + @Basic + @Column(name = "villagename") + public String getVillagename() { + return villagename; + } + + public void setVillagename(String villagename) { + this.villagename = villagename; + } + + @Override + public int hashCode() { + return Objects.hash(userId, usrMappingId, name, userName, serviceId, serviceName, isNational, stateId, stateName, workingDistrictId, workingDistrictName, workingLocationId, serviceProviderId, locationName, workingLocationAddress, roleId, roleName, providerServiceMapId, agentId, psmStatusId, psmStatus, userServciceRoleDeleted, userDeleted, serviceProviderDeleted, roleDeleted, providerServiceMappingDeleted, isInbound, isOutbound, blockid, blockname, villageid, villagename); + } +} diff --git a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java index 3415d91a..e41f8e80 100644 --- a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java +++ b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java @@ -18,6 +18,8 @@ public class FieldResponseDTO { private String defaultValue; private String placeholder; private Integer sequence; + private Boolean isEditable; + private Integer stateCode; private List options; private Map validation; private Map conditional; diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java new file mode 100644 index 00000000..ee7db934 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -0,0 +1,16 @@ +package com.iemr.common.repository.users; + +import com.iemr.common.data.users.UserServiceRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserServiceRoleRepo extends JpaRepository { + UserServiceRole findByUserName(String userName); + UserServiceRole findByUserId(Integer userId); + +} diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java index 6d22e59a..2f7360b8 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java @@ -16,7 +16,7 @@ public interface FormMasterService { List createField(List dto); FormField updateField(FieldDTO dto); - FormResponseDTO getStructuredFormByFormId(String formId,String lang); + FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token); void deleteField(Long fieldId); } 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 98d93dbe..27dd5d08 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -7,23 +7,29 @@ import com.iemr.common.data.dynamic_from.FormField; import com.iemr.common.data.dynamic_from.FormModule; import com.iemr.common.data.translation.Translation; +import com.iemr.common.data.users.UserServiceRole; +import com.iemr.common.data.users.UserServiceRoleMapping; import com.iemr.common.dto.dynamicForm.*; import com.iemr.common.repository.dynamic_form.FieldRepository; import com.iemr.common.repository.dynamic_form.FormRepository; import com.iemr.common.repository.dynamic_form.ModuleRepository; import com.iemr.common.repository.translation.TranslationRepo; +import com.iemr.common.repository.users.UserRoleMappingRepository; +import com.iemr.common.repository.users.UserServiceRoleRepo; +import com.iemr.common.utils.JwtUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import javax.persistence.criteria.CriteriaBuilder; +import java.util.*; import java.util.stream.Collectors; @Service public class FormMasterServiceImpl implements FormMasterService { + final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired private ModuleRepository moduleRepo; @@ -33,6 +39,12 @@ public class FormMasterServiceImpl implements FormMasterService { @Autowired private TranslationRepo translationRepo; + @Autowired + private UserServiceRoleRepo userServiceRoleRepo; + + @Autowired + private JwtUtil jwtUtil; + @Override public FormModule createModule(ModuleDTO dto) { FormModule module = new FormModule(); @@ -103,99 +115,115 @@ public FormField updateField(FieldDTO dto) { } @Override - public FormResponseDTO getStructuredFormByFormId(String formId,String lang) { - FormDefinition form = formRepo.findByFormId(formId) - .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); + public FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token) { + int stateId =0 ; + + try { + UserServiceRole userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); + if(userServiceRole!=null){ + stateId = userServiceRole.getStateId(); + } + FormDefinition form = formRepo.findByFormId(formId) + .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); - ObjectMapper objectMapper = new ObjectMapper(); + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); + ObjectMapper objectMapper = new ObjectMapper(); - List fieldDtos = fields.stream() - .map(field -> { - String labelKey = field.getFieldId(); // field label already contains label_key + Integer finalStateId = stateId; + List fieldDtos = fields.stream().filter(formField -> Objects.equals(formField.getStateCode(), finalStateId) || formField.getStateCode()==0) + .map(field -> { + String labelKey = field.getFieldId(); // field label already contains label_key - Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) - .orElse(null); + Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) + .orElse(null); - String translatedLabel = field.getLabel(); // fallback + String translatedLabel = field.getLabel(); // fallback - if (t != null) { - if ("hi".equalsIgnoreCase(lang)) { - translatedLabel = t.getHindiTranslation(); - } else if("as".equalsIgnoreCase(lang)){ - translatedLabel = t.getAssameseTranslation(); - }else if("en".equalsIgnoreCase(lang)){ - translatedLabel = t.getEnglish(); + if (t != null) { + if ("hi".equalsIgnoreCase(lang)) { + translatedLabel = t.getHindiTranslation(); + } else if("as".equalsIgnoreCase(lang)){ + translatedLabel = t.getAssameseTranslation(); + }else if("en".equalsIgnoreCase(lang)){ + translatedLabel = t.getEnglish(); - } - } - - FieldResponseDTO dto = new FieldResponseDTO(); - dto.setId(field.getId()); - dto.setVisible(field.getIsVisible()); - dto.setFormId(field.getForm().getFormId()); - dto.setSectionTitle(field.getSectionTitle()); - dto.setFieldId(field.getFieldId()); - dto.setLabel(translatedLabel); - dto.setType(field.getType()); - dto.setIsRequired(field.getIsRequired()); - dto.setDefaultValue(field.getDefaultValue()); - dto.setPlaceholder(field.getPlaceholder()); - dto.setSequence(field.getSequence()); - - try { - // Handle options - if (field.getOptions() != null && !field.getOptions().isBlank()) { - JsonNode node = objectMapper.readTree(field.getOptions()); - List options = null; - if (node.isArray()) { - options = objectMapper.convertValue(node, new TypeReference<>() {}); - } else if (node.has("options")) { - options = objectMapper.convertValue(node.get("options"), new TypeReference<>() {}); } - dto.setOptions(options == null || options.isEmpty() ? null : options); - } else { - dto.setOptions(null); } - // Handle validation - if (field.getValidation() != null && !field.getValidation().isBlank()) { - Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() {}); - dto.setValidation(validation.isEmpty() ? null : validation); - } else { - dto.setValidation(null); - } + FieldResponseDTO dto = new FieldResponseDTO(); + dto.setId(field.getId()); + dto.setIsEditable(field.getIsEditable()); + dto.setStateCode(field.getStateCode()); + dto.setVisible(field.getIsVisible()); + dto.setFormId(field.getForm().getFormId()); + dto.setSectionTitle(field.getSectionTitle()); + dto.setFieldId(field.getFieldId()); + dto.setLabel(translatedLabel); + dto.setType(field.getType()); + dto.setIsRequired(field.getIsRequired()); + dto.setDefaultValue(field.getDefaultValue()); + dto.setPlaceholder(field.getPlaceholder()); + dto.setSequence(field.getSequence()); + + try { + // Handle options + if (field.getOptions() != null && !field.getOptions().isBlank()) { + JsonNode node = objectMapper.readTree(field.getOptions()); + List options = null; + if (node.isArray()) { + options = objectMapper.convertValue(node, new TypeReference<>() {}); + } else if (node.has("options")) { + options = objectMapper.convertValue(node.get("options"), new TypeReference<>() {}); + } + dto.setOptions(options == null || options.isEmpty() ? null : options); + } else { + dto.setOptions(null); + } + + // Handle validation + if (field.getValidation() != null && !field.getValidation().isBlank()) { + Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() {}); + dto.setValidation(validation.isEmpty() ? null : validation); + } else { + dto.setValidation(null); + } - // Handle conditional - if (field.getConditional() != null && !field.getConditional().isBlank()) { - Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() {}); - dto.setConditional(conditional.isEmpty() ? null : conditional); - } else { - dto.setConditional(null); + // Handle conditional + if (field.getConditional() != null && !field.getConditional().isBlank()) { + Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() {}); + dto.setConditional(conditional.isEmpty() ? null : conditional); + } else { + dto.setConditional(null); + } + } catch (Exception e) { + + System.err.println("JSON Parsing Error in field: " + field.getFieldId()); + throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); } - } catch (Exception e) { - System.err.println("JSON Parsing Error in field: " + field.getFieldId()); - throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); - } + return dto; + }) + .sorted(Comparator.comparing(FieldResponseDTO::getId)) + .collect(Collectors.toList()); + - return dto; - }) - .sorted(Comparator.comparing(FieldResponseDTO::getId)) - .collect(Collectors.toList()); + GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); + singleSection.setSectionTitle(singleSection.getSectionTitle()); // your custom section title + singleSection.setFields(fieldDtos); + FormResponseDTO response = new FormResponseDTO(); + response.setVersion(form.getVersion()); + response.setFormId(form.getFormId()); + response.setFormName(form.getFormName()); + response.setSections(List.of(singleSection)); + return response; - GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); - singleSection.setSectionTitle(singleSection.getSectionTitle()); // your custom section title - singleSection.setFields(fieldDtos); + }catch (Exception e){ + logger.error("Exception:"+e.getMessage()); + } - FormResponseDTO response = new FormResponseDTO(); - response.setVersion(form.getVersion()); - response.setFormId(form.getFormId()); - response.setFormName(form.getFormName()); - response.setSections(List.of(singleSection)); + return null; - return response; } From 4d20b8cf8be32079cd84ec21c4c461f24f32a752 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 14 Jan 2026 16:46:22 +0530 Subject: [PATCH 15/70] implement state wise hide un hide form fields --- .../common/controller/dynamicForm/DynamicFormController.java | 1 + .../java/com/iemr/common/data/users/UserServiceRole.java | 1 + .../iemr/common/repository/dynamic_form/FieldRepository.java | 5 +++++ .../iemr/common/repository/users/UserServiceRoleRepo.java | 1 + .../iemr/common/service/dynamicForm/FormMasterService.java | 1 + .../common/service/dynamicForm/FormMasterServiceImpl.java | 5 ++--- 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index cd83246f..3d49e808 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -97,4 +97,5 @@ public ResponseEntity> getStructuredForm(@PathVariable String for } + } diff --git a/src/main/java/com/iemr/common/data/users/UserServiceRole.java b/src/main/java/com/iemr/common/data/users/UserServiceRole.java index 63af40dd..27c044cf 100644 --- a/src/main/java/com/iemr/common/data/users/UserServiceRole.java +++ b/src/main/java/com/iemr/common/data/users/UserServiceRole.java @@ -365,4 +365,5 @@ public void setVillagename(String villagename) { public int hashCode() { return Objects.hash(userId, usrMappingId, name, userName, serviceId, serviceName, isNational, stateId, stateName, workingDistrictId, workingDistrictName, workingLocationId, serviceProviderId, locationName, workingLocationAddress, roleId, roleName, providerServiceMapId, agentId, psmStatusId, psmStatus, userServciceRoleDeleted, userDeleted, serviceProviderDeleted, roleDeleted, providerServiceMappingDeleted, isInbound, isOutbound, blockid, blockname, villageid, villagename); } + } diff --git a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java index 4aea5698..20a86e3e 100644 --- a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java +++ b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java @@ -9,4 +9,9 @@ @Repository public interface FieldRepository extends JpaRepository { List findByForm_FormIdOrderBySequenceAsc(String formId); + List findByForm_FormIdAndStateCodeOrderBySequenceAsc( + String formId, + Integer stateCode + ); + } diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java index ee7db934..798f47c1 100644 --- a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -13,4 +13,5 @@ public interface UserServiceRoleRepo extends JpaRepository new IllegalArgumentException("Invalid form ID")); - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); + List fields = fieldRepo.findByForm_FormIdAndStateCodeOrderBySequenceAsc(formId,stateId); ObjectMapper objectMapper = new ObjectMapper(); - Integer finalStateId = stateId; - List fieldDtos = fields.stream().filter(formField -> Objects.equals(formField.getStateCode(), finalStateId) || formField.getStateCode()==0) + List fieldDtos = fields.stream() .map(field -> { String labelKey = field.getFieldId(); // field label already contains label_key From bed849b895b9aca8e121696717e3668bd55ac91c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 16 Jan 2026 20:18:50 +0530 Subject: [PATCH 16/70] implement state wise hide un hide form fields --- .../common/controller/dynamicForm/DynamicFormController.java | 1 + src/main/java/com/iemr/common/data/translation/Translation.java | 1 + .../com/iemr/common/repository/dynamic_form/FieldRepository.java | 1 + .../com/iemr/common/repository/translation/TranslationRepo.java | 1 + .../com/iemr/common/repository/users/UserServiceRoleRepo.java | 1 + .../com/iemr/common/service/dynamicForm/FormMasterService.java | 1 + .../iemr/common/service/dynamicForm/FormMasterServiceImpl.java | 1 - 7 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index 3d49e808..6f64a135 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -98,4 +98,5 @@ public ResponseEntity> getStructuredForm(@PathVariable String for + } 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..52cb8027 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -22,4 +22,5 @@ public class Translation { private String assameseTranslation; @Column(name = "is_active") private Boolean isActive; + } diff --git a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java index 20a86e3e..50e84248 100644 --- a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java +++ b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java @@ -14,4 +14,5 @@ List findByForm_FormIdAndStateCodeOrderBySequenceAsc( Integer stateCode ); + } diff --git a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java index f6a5dcb0..139b5ee9 100644 --- a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java +++ b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java @@ -10,4 +10,5 @@ public interface TranslationRepo extends JpaRepository { Optional findByLabelKeyAndIsActive(String labelKey, boolean isActive); + } diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java index 798f47c1..17edb5e0 100644 --- a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -14,4 +14,5 @@ public interface UserServiceRoleRepo extends JpaRepository Date: Sun, 18 Jan 2026 11:30:45 +0530 Subject: [PATCH 17/70] enhance welcome sms code --- .../com/iemr/common/CommonApplication.java | 2 + .../RegisterBenificiaryServiceImpl.java | 25 ++++++- .../WelcomeBenificarySmsServiceImpl.java | 72 +++++++++---------- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/iemr/common/CommonApplication.java b/src/main/java/com/iemr/common/CommonApplication.java index e4a59994..dff5d809 100644 --- a/src/main/java/com/iemr/common/CommonApplication.java +++ b/src/main/java/com/iemr/common/CommonApplication.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; @@ -40,6 +41,7 @@ @SpringBootApplication @EnableScheduling +@EnableAsync public class CommonApplication extends SpringBootServletInitializer { @Bean diff --git a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java index 54637a4c..388d6298 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java @@ -29,6 +29,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import com.iemr.common.model.beneficiary.RMNCHBeneficiaryDetailsRmnch; import com.iemr.common.service.welcomeSms.WelcomeBenificarySmsService; @@ -234,9 +235,27 @@ public String save(BeneficiaryModel beneficiaryModel, HttpServletRequest servlet } else { return response.toString(); } - if(beneficiary!=null){ - if(beneficiary.getBenPhoneMaps().get(0).getPhoneNo()!=null){ - welcomeBenificarySmsService.sendWelcomeSMStoBenificiary(beneficiary.getBenPhoneMaps().get(0).getPhoneNo(),beneficiary.getFirstName()+" "+beneficiary.getLastName(),beneficiary.getBeneficiaryID()); + // ========== SEND SMS BUT DON'T FAIL IF IT ERRORS ========== + if (beneficiary != null && beneficiary.getBenPhoneMaps() != null && !beneficiary.getBenPhoneMaps().isEmpty()) { + String phoneNo = beneficiary.getBenPhoneMaps().get(0).getPhoneNo(); + + if (phoneNo != null && !phoneNo.trim().isEmpty()) { + String beneficiaryName = (beneficiary.getFirstName() != null ? beneficiary.getFirstName() : "") + " " + + (beneficiary.getLastName() != null ? beneficiary.getLastName() : ""); + + try { + logger.info("[SMS] Attempting to send welcome SMS to: " + phoneNo); + String smsResult = welcomeBenificarySmsService.sendWelcomeSMStoBenificiary( + phoneNo, + beneficiaryName.trim(), + beneficiary.getBeneficiaryID() + ); + logger.info("[SMS] Result: " + smsResult); + } catch (Exception smsError) { + // SMS failed but beneficiary is already created - don't fail the request + logger.warn("[SMS] Failed to send SMS: " + smsError.getMessage() + + " - But beneficiary already created successfully"); + } } } diff --git a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java index 67b642ab..a80a98e8 100644 --- a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java +++ b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -46,58 +47,57 @@ public class WelcomeBenificarySmsServiceImpl implements WelcomeBenificarySmsServ private String smsTemplateName = "welcome_sms"; - private String smsTemplate; + private String smsTemplate =null; @Override + @Async public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryName, String beneficiaryId) { - final RestTemplate restTemplate = new RestTemplate(); - - Optional smsTemplateData = smsTemplateRepository.findBySmsTemplateName(smsTemplateName); - if (smsTemplateData.isPresent()) { - smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplateData.get().getSmsTemplateID()).getSmsTemplate(); - - } - - logger.info("sms template" + smsTemplate); - - - String sendSMSAPI = SMS_GATEWAY_URL; try { + String sendSMSAPI = SMS_GATEWAY_URL; + + final RestTemplate restTemplate = new RestTemplate(); - String message = smsTemplate.replace("$$BENE_NAME$$", beneficiaryName).replace("$$BEN_ID$$", beneficiaryId); - // Build payload - Map payload = new HashMap<>(); - payload.put("customerId", smsUserName); - payload.put("destinationAddress", contactNo); - payload.put("message", message); - payload.put("sourceAddress", smsSourceAddress); - payload.put("messageType", "SERVICE_IMPLICIT"); - payload.put("dltTemplateId", smsTemplateData.get().getDltTemplateId()); - payload.put("entityId", smsEntityId); - // Set headers - HttpHeaders headers = new HttpHeaders(); - String auth = smsUserName + ":" + smsPassword; - headers.add("Authorization", - "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); + Optional smsTemplateData = smsTemplateRepository.findBySmsTemplateName(smsTemplateName); + if (smsTemplateData.isPresent()) { + smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplateData.get().getSmsTemplateID()).getSmsTemplate(); + } + if(smsTemplate!=null){ + String message = smsTemplate.replace("$$BENE_NAME$$", beneficiaryName).replace("$$BEN_ID$$", beneficiaryId); + // Build payload + Map payload = new HashMap<>(); + payload.put("customerId", smsUserName); + payload.put("destinationAddress", contactNo); + payload.put("message", message); + payload.put("sourceAddress", smsSourceAddress); + payload.put("messageType", "SERVICE_IMPLICIT"); + payload.put("dltTemplateId", smsTemplateData.get().getDltTemplateId()); + payload.put("entityId", smsEntityId); + // Set headers + HttpHeaders headers = new HttpHeaders(); + String auth = smsUserName + ":" + smsPassword; + headers.add("Authorization", + "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); headers.setContentType(MediaType.APPLICATION_JSON); logger.info("payload: " + payload); HttpEntity> request = new HttpEntity<>(payload, headers); - // Call API - ResponseEntity response = restTemplate.postForEntity(sendSMSAPI, request, String.class); - logger.info("sms-response:" + response.getBody()); - if (response.getStatusCode().value() == 200) { - return "OTP sent successfully on register mobile number"; - } else { - return "Fail"; + // Call API + ResponseEntity response = restTemplate.postForEntity(sendSMSAPI, request, String.class); + logger.info("sms-response:" + response.getBody()); + if (response.getStatusCode().value() == 200) { + return "OTP sent successfully on register mobile number"; + } else { + return "Fail"; + } } + } catch (Exception e) { return "Error sending SMS: " + e.getMessage().toString(); } - + return null; } } From 4104ad034e2983d7e11f5696057a607181e579ec Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Sun, 18 Jan 2026 23:15:23 +0530 Subject: [PATCH 18/70] fix hide unhide form issue --- .../common/service/dynamicForm/FormMasterServiceImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 2edd805c..4e952a68 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -125,10 +125,11 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin FormDefinition form = formRepo.findByFormId(formId) .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - List fields = fieldRepo.findByForm_FormIdAndStateCodeOrderBySequenceAsc(formId,stateId); + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); ObjectMapper objectMapper = new ObjectMapper(); - List fieldDtos = fields.stream() + int finalStateId = stateId; + List fieldDtos = fields.stream().filter(formField -> formField.getStateCode()== 0 || formField.getStateCode()== finalStateId) .map(field -> { String labelKey = field.getFieldId(); // field label already contains label_key From 7280c422e02eddc681dc5cf2f277639b07879e08 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 19 Jan 2026 13:22:40 +0530 Subject: [PATCH 19/70] docs: add DeepWiki badge and documentation link --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 59d3361b..2264b9e1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # AMRIT - Common Service -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ![branch parameter](https://github.com/PSMRI/Common-API/actions/workflows/sast-and-package.yml/badge.svg) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) Common API is a microservice whch acts as a gateway for AMRIT. There are many APIs that are exposed by Common-API. It contains APIs of common integrators like c-Zentrix, Everwell, Openkm and some master APIs like location master, alerts, notification,language and location messages. @@ -88,3 +88,6 @@ If you encounter any issues, bugs, or have feature requests, please file them in We’d love to have you join our community discussions and get real-time support! Join our [Discord server](https://discord.gg/FVQWsf5ENS) to connect with contributors, ask questions, and stay updated. +## Documentation + +[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI/Common--API-blue)](https://deepwiki.com/PSMRI/Common-API) \ No newline at end of file From 61c89c472e7e669c48c31c363eca6f8c1b6188d9 Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:10:01 +0530 Subject: [PATCH 20/70] Add DeepWiki badge to README Added DeepWiki badge to README for better visibility. --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 2264b9e1..169fc005 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # AMRIT - Common Service [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI/Common--API-blue)](https://deepwiki.com/PSMRI/Common-API) Common API is a microservice whch acts as a gateway for AMRIT. There are many APIs that are exposed by Common-API. It contains APIs of common integrators like c-Zentrix, Everwell, Openkm and some master APIs like location master, alerts, notification,language and location messages. @@ -87,7 +88,3 @@ If you encounter any issues, bugs, or have feature requests, please file them in We’d love to have you join our community discussions and get real-time support! Join our [Discord server](https://discord.gg/FVQWsf5ENS) to connect with contributors, ask questions, and stay updated. - -## Documentation - -[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI/Common--API-blue)](https://deepwiki.com/PSMRI/Common-API) \ No newline at end of file From fdbc9b762da293fe4763e3ef45206c332400de79 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 17:46:01 +0530 Subject: [PATCH 21/70] fix hide unhide form issue --- .../common/service/dynamicForm/FormMasterServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 27dd5d08..a4db6b46 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -126,11 +126,11 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin FormDefinition form = formRepo.findByFormId(formId) .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); + int finalStateId = stateId; + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId).stream().filter(formField -> (formField.getStateCode() == 0 || formField.getStateCode() == finalStateId)).toList(); ObjectMapper objectMapper = new ObjectMapper(); - Integer finalStateId = stateId; - List fieldDtos = fields.stream().filter(formField -> Objects.equals(formField.getStateCode(), finalStateId) || formField.getStateCode()==0) + List fieldDtos = fields.stream() .map(field -> { String labelKey = field.getFieldId(); // field label already contains label_key From 0c64cef2ec7456507645d96726038f51f41f5c27 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 17:58:48 +0530 Subject: [PATCH 22/70] fix hide unhide form issue --- .../iemr/common/service/dynamicForm/FormMasterServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 a4db6b46..645bc272 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -126,8 +126,7 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin FormDefinition form = formRepo.findByFormId(formId) .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - int finalStateId = stateId; - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId).stream().filter(formField -> (formField.getStateCode() == 0 || formField.getStateCode() == finalStateId)).toList(); + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); ObjectMapper objectMapper = new ObjectMapper(); List fieldDtos = fields.stream() From b3a1922544e11e181c4ac043cac897d0c1c54e4c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:11:51 +0530 Subject: [PATCH 23/70] fix hide unhide form issue --- .../service/dynamicForm/FormMasterServiceImpl.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 645bc272..9d3b14e3 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -122,7 +122,16 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin UserServiceRole userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); if(userServiceRole!=null){ stateId = userServiceRole.getStateId(); + logger.info("State:Id"+stateId); } + if(!token.isEmpty()){ + logger.info("Token: "+token); + + } + + logger.info("State outSide: "+stateId); + + FormDefinition form = formRepo.findByFormId(formId) .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); From c4c1d48f79184173906838814177e81530c76d24 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:17:59 +0530 Subject: [PATCH 24/70] fix hide unhide form issue --- .../common/controller/dynamicForm/DynamicFormController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index cd83246f..62bf7e7c 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -84,7 +84,7 @@ public ResponseEntity> deleteField(@PathVariable Long fieldId) { } @GetMapping(value = "form/{formId}/fields") - public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang,@RequestHeader(value = "JwtToken") String token) { + public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang,@RequestHeader(value = "jwttoken") String token) { try { Object result = formMasterService.getStructuredFormByFormId(formId,lang,token); return ResponseEntity.status(HttpStatus.OK) From f10f37cb86948e657f84c4663627403b70d05930 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:23:40 +0530 Subject: [PATCH 25/70] fix hide unhide form issue --- .../service/dynamicForm/FormMasterServiceImpl.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 9d3b14e3..7ce3e01d 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -119,11 +119,14 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin int stateId =0 ; try { - UserServiceRole userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); - if(userServiceRole!=null){ - stateId = userServiceRole.getStateId(); - logger.info("State:Id"+stateId); + if(!token.isEmpty()){ + UserServiceRole userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); + if(userServiceRole!=null){ + stateId = userServiceRole.getStateId(); + logger.info("State:Id"+stateId); + } } + if(!token.isEmpty()){ logger.info("Token: "+token); From b3da893ff0fb540203939c57f0527565e56ebcc7 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:32:07 +0530 Subject: [PATCH 26/70] fix hide unhide form issue --- .../dynamicForm/FormMasterServiceImpl.java | 16 ---------------- 1 file changed, 16 deletions(-) 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 7ce3e01d..e61f7b17 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -116,23 +116,7 @@ public FormField updateField(FieldDTO dto) { @Override public FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token) { - int stateId =0 ; - try { - if(!token.isEmpty()){ - UserServiceRole userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); - if(userServiceRole!=null){ - stateId = userServiceRole.getStateId(); - logger.info("State:Id"+stateId); - } - } - - if(!token.isEmpty()){ - logger.info("Token: "+token); - - } - - logger.info("State outSide: "+stateId); FormDefinition form = formRepo.findByFormId(formId) From f94d0b07cc4ebfc6c4cdb54b13a0df6dd7e14d75 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:40:18 +0530 Subject: [PATCH 27/70] fix hide unhide form issue --- .../common/repository/users/UserServiceRoleRepo.java | 2 +- .../service/dynamicForm/FormMasterServiceImpl.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java index ee7db934..65691053 100644 --- a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -10,7 +10,7 @@ @Repository public interface UserServiceRoleRepo extends JpaRepository { - UserServiceRole findByUserName(String userName); + List findByUserName(String userName); UserServiceRole findByUserId(Integer userId); } 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 e61f7b17..93a8d209 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -116,7 +116,16 @@ public FormField updateField(FieldDTO dto) { @Override public FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token) { + int stateId =0 ; + try { + if(!token.isEmpty()){ + List userServiceRole= userServiceRoleRepo.findByUserName(jwtUtil.getUsernameFromToken(token)); + if(userServiceRole!=null){ + stateId = userServiceRole.get(0).getStateId(); + logger.info("State:Id"+stateId); + } + } FormDefinition form = formRepo.findByFormId(formId) @@ -125,7 +134,8 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); ObjectMapper objectMapper = new ObjectMapper(); - List fieldDtos = fields.stream() + int finalStateId = stateId; + List fieldDtos = fields.stream().filter(formField -> (formField.getStateCode()==0 || formField.getStateCode().equals(finalStateId))) .map(field -> { String labelKey = field.getFieldId(); // field label already contains label_key From be7889e1407544df7fe91c48ffa35b83d28003b1 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:48:20 +0530 Subject: [PATCH 28/70] fix hide unhide form issue --- .../java/com/iemr/common/config/encryption/SecurePassword.java | 3 +++ .../iemr/common/service/dynamicForm/FormMasterServiceImpl.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java index 15463b7a..7aa9c6fd 100644 --- a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java +++ b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java @@ -30,6 +30,8 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; @Service @@ -149,4 +151,5 @@ private byte[] fromHex(String hex) { } return bytes; } + } 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 93a8d209..512ad015 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -128,6 +128,7 @@ public FormResponseDTO getStructuredFormByFormId(String formId,String lang,Strin } + FormDefinition form = formRepo.findByFormId(formId) .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); From 00223293fed97811c81cec301268b20cc56813e8 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:48:40 +0530 Subject: [PATCH 29/70] fix hide unhide form issue --- .../com/iemr/common/repository/users/UserServiceRoleRepo.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java index 65691053..111fcf43 100644 --- a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -11,6 +11,5 @@ @Repository public interface UserServiceRoleRepo extends JpaRepository { List findByUserName(String userName); - UserServiceRole findByUserId(Integer userId); } From ba824bee7ce2cc2717b8c0298dc08513057748cf Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:50:02 +0530 Subject: [PATCH 30/70] fix hide unhide form issue --- .../com/iemr/common/config/encryption/SecurePassword.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java index 7aa9c6fd..9b717375 100644 --- a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java +++ b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java @@ -29,9 +29,6 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; @Service @@ -151,5 +148,4 @@ private byte[] fromHex(String hex) { } return bytes; } - } From c0c53333b8ba76f52e1e3bcbe1eb658ef30bcb4a Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:51:26 +0530 Subject: [PATCH 31/70] fix hide unhide form issue --- src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4409afd4..9fb04c9d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -372,5 +372,4 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent -cors.allowed-origin=https://amritdemo.piramalswasthya.org From 2e18fddcdd254a4ce7ff7ab0ec02df4a9445b9a1 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:52:29 +0530 Subject: [PATCH 32/70] fix hide unhide form issue --- .../java/com/iemr/common/config/encryption/SecurePassword.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java index 9b717375..95cdd7f3 100644 --- a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java +++ b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java @@ -26,7 +26,6 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; - import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.springframework.stereotype.Service; From acd9d8e9d7348ddfb43e34330af4537ea262e399 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:53:58 +0530 Subject: [PATCH 33/70] fix hide unhide form issue --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 11ad9f37..171ab162 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.0 + 3.6.1 war Common-API From 475a4acae64e598d980de47a89a700d5ae4ac929 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:55:10 +0530 Subject: [PATCH 34/70] fix hide unhide form issue --- 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 9fb04c9d..6b52b9b5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -372,4 +372,4 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent - +cors.allowed-origin = From f95270843c29a9037ee5fe105c91c3c2556070dc Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:55:47 +0530 Subject: [PATCH 35/70] fix hide unhide form issue --- src/main/resources/application.properties | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6b52b9b5..eee27bbc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -76,14 +76,14 @@ get-details-call-report-URL=http://CTI_SERVER/apps/customize_apps/piramil_report #============================================================================ # Configure Main Scheduler Properties #============================================================================ - + org.quartz.scheduler.instanceId = AUTO org.quartz.scheduler.makeSchedulerThreadDaemon = true - + #============================================================================ # Configure ThreadPool #============================================================================ - + org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.makeThreadsDaemons = true org.quartz.threadPool.threadCount: 20 @@ -110,6 +110,7 @@ iemr.extend.expiry.time.changePassword=true iemr.session.expiry.time.changePassword=600 identity-api-url-advancesearch =IDENTITY_BASE_URL/id/advanceSearch +identity-api-url-advancesearch-es =IDENTITY_BASE_URL/beneficiary/advancedSearchES identity-api-url-getByBenRegIdList =IDENTITY_BASE_URL/id/getByBenRegIdList identity-api-url-getByPartialBenRegIdList =IDENTITY_BASE_URL/id/getByPartialBenRegIdList identity-api-url-getByPhoneNum =IDENTITY_BASE_URL/id/getByPhoneNum?phoneNum= @@ -118,6 +119,7 @@ identity-api-url-getByBenRegId =IDENTITY_BASE_URL/id/getByBenRegId?benRegId= identity-api-url-benCreate =IDENTITY_BASE_URL/id/create identity-api-url-benEdit =IDENTITY_BASE_URL/id/edit identity-api-url-benEditEducationCommunity=IDENTITY_BASE_URL/id/editEducationOrCommunity +identity-api-url-searchByES=IDENTITY_BASE_URL/beneficiary/search identity-api-url-getByFamilyId=IDENTITY_BASE_URL/id/searchByFamilyId?familyId= identity-api-url-getByGovIdentity=IDENTITY_BASE_URL/id/searchByGovIdentity?identity= @@ -372,4 +374,5 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent -cors.allowed-origin = + +cors.allowed-origin = \ No newline at end of file From 64b2a7e8eb7a4378e1e4ebfddb048b3975ffb44c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 21 Jan 2026 18:56:43 +0530 Subject: [PATCH 36/70] fix hide unhide form issue --- src/main/resources/application.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index eee27bbc..beefff31 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -110,7 +110,6 @@ iemr.extend.expiry.time.changePassword=true iemr.session.expiry.time.changePassword=600 identity-api-url-advancesearch =IDENTITY_BASE_URL/id/advanceSearch -identity-api-url-advancesearch-es =IDENTITY_BASE_URL/beneficiary/advancedSearchES identity-api-url-getByBenRegIdList =IDENTITY_BASE_URL/id/getByBenRegIdList identity-api-url-getByPartialBenRegIdList =IDENTITY_BASE_URL/id/getByPartialBenRegIdList identity-api-url-getByPhoneNum =IDENTITY_BASE_URL/id/getByPhoneNum?phoneNum= @@ -119,7 +118,6 @@ identity-api-url-getByBenRegId =IDENTITY_BASE_URL/id/getByBenRegId?benRegId= identity-api-url-benCreate =IDENTITY_BASE_URL/id/create identity-api-url-benEdit =IDENTITY_BASE_URL/id/edit identity-api-url-benEditEducationCommunity=IDENTITY_BASE_URL/id/editEducationOrCommunity -identity-api-url-searchByES=IDENTITY_BASE_URL/beneficiary/search identity-api-url-getByFamilyId=IDENTITY_BASE_URL/id/searchByFamilyId?familyId= identity-api-url-getByGovIdentity=IDENTITY_BASE_URL/id/searchByGovIdentity?identity= @@ -375,4 +373,4 @@ allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent sms-template-name = otp_consent -cors.allowed-origin = \ No newline at end of file +cors.allowed-origin = From f60f36e93b95cff9d6d47dfd0351cd2a7de5ad7b Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:05:46 +0530 Subject: [PATCH 37/70] chore(swagger): automate swagger sync to amrit-docs (#354) * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs --- .github/workflows/swagger-json.yml | 89 +++++++++++++++++++ pom.xml | 6 ++ .../iemr/common/config/PrimaryDBConfig.java | 4 +- .../iemr/common/config/SecondaryDBConfig.java | 5 +- .../ScheduleJobForNHMDashboardData.java | 2 + .../BeneficiaryRegistrationController.java | 2 + .../CustomerRelationshipSecondaryReports.java | 2 + .../callreport/CallReportSecondaryRepo.java | 2 + .../BenRelationshipTypeServiceImpl.java | 2 + .../BeneficiaryOccupationServiceImpl.java | 2 + .../RegisterBenificiaryServiceImpl.java | 3 +- .../SexualOrientationServiceImpl.java | 3 +- .../SecondaryReportService.java | 1 + .../SecondaryReportServiceImpl.java | 2 + .../com/iemr/common/utils/FilterConfig.java | 1 + .../common/utils/JwtAuthenticationUtil.java | 1 + .../data/report/SecondaryCallReport.java | 2 + .../resources/application-swagger.properties | 28 ++++++ src/main/resources/application.properties | 4 +- 19 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/swagger-json.yml create mode 100644 src/main/resources/application-swagger.properties diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml new file mode 100644 index 00000000..bac1707d --- /dev/null +++ b/.github/workflows/swagger-json.yml @@ -0,0 +1,89 @@ +name: Sync Swagger to AMRIT-Docs + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + swagger-sync: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout API repo + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + + - name: Build API (skip tests) + run: mvn clean package -DskipTests + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Run API in swagger profile + run: | + mvn spring-boot:run \ + -Dspring-boot.run.profiles=swagger \ + -Dspring-boot.run.arguments=--server.port=9090 \ + > app.log 2>&1 & + echo $! > api_pid.txt + + - name: Wait for API & fetch Swagger + run: | + for i in {1..30}; do + CODE=$(curl --connect-timeout 2 --max-time 5 -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + if [ "$CODE" = "200" ]; then + if jq . swagger_raw.json > common-api.json; then + echo "Swagger generated successfully" + exit 0 + else + echo "Failed to parse swagger_raw.json with jq" + exit 1 + fi + fi + echo "Waiting for API... ($i)" + sleep 5 + done + + echo "Swagger not generated" + cat app.log || true + exit 1 + + - name: Stop API + if: always() + run: | + if [ -f api_pid.txt ]; then + kill $(cat api_pid.txt) || true + fi + + - name: Checkout AMRIT-Docs + uses: actions/checkout@v4 + with: + repository: PSMRI/AMRIT-Docs + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + + - name: Copy Swagger JSON + run: | + mkdir -p amrit-docs/docs/swagger + cp common-api.json amrit-docs/docs/swagger/common-api.json + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + branch: auto/swagger-update-${{ github.run_id }} + base: main + commit-message: Auto-update Common-API swagger + title: Auto-update Common-API swagger + body: | + This PR automatically updates the Common-API Swagger JSON + from the latest main branch build. diff --git a/pom.xml b/pom.xml index 11ad9f37..f818762c 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,12 @@ + + + com.h2database + h2 + runtime + org.springframework.boot diff --git a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java index 36463ab9..8a77a74a 100644 --- a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java @@ -39,6 +39,7 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.context.annotation.Profile; import com.iemr.common.utils.config.ConfigProperties; @@ -47,7 +48,8 @@ @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory", basePackages = { "com.iemr.common.repository", - "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) + "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) +@Profile("!swagger") public class PrimaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java index 8a3928cb..3244612f 100644 --- a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java @@ -43,10 +43,13 @@ import jakarta.persistence.EntityManagerFactory; +import org.springframework.context.annotation.Profile; + @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager", basePackages = { - "com.iemr.common.secondary.repository.callreport" }) + "com.iemr.common.secondary.repository.callreport" }) +@Profile("!swagger") public class SecondaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java index fda36f0d..c9b29c62 100644 --- a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java +++ b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java @@ -31,9 +31,11 @@ import org.springframework.transaction.annotation.Transactional; import com.iemr.common.service.nhm_dashboard.NHM_DashboardService; +import org.springframework.context.annotation.Profile; @Service @Transactional +@Profile("!swagger") public class ScheduleJobForNHMDashboardData implements Job { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 8f573d6d..3d9e204e 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -39,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.context.annotation.Profile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -80,6 +81,7 @@ @RequestMapping({ "/beneficiary" }) @RestController +@Profile("!swagger") public class BeneficiaryRegistrationController { private InputMapper inputMapper = new InputMapper(); diff --git a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java index 47d73255..88a8c863 100644 --- a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java +++ b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.databind.ObjectMapper; @@ -46,6 +47,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; +@Profile("!swagger") @RequestMapping({ "/crmReports" }) @RestController public class CustomerRelationshipSecondaryReports { diff --git a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java index 0a5dee7f..8e7e7ea8 100644 --- a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java +++ b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java @@ -27,10 +27,12 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import common.iemr.common.secondary.data.report.SecondaryCallReport; +@Profile("!swagger") @Repository public interface CallReportSecondaryRepo extends CrudRepository { @Query(value="call Pr_104QAReport(:startDateTime,:endDateTime,:receivedRoleName,:agentID,:providerServiceMapID)", nativeQuery=true) diff --git a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java index 01a98eb2..31d9b227 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java @@ -31,7 +31,9 @@ import com.iemr.common.data.beneficiary.BenRelationshipType; import com.iemr.common.repository.beneficiary.BeneficiaryRelationshipTypeRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BenRelationshipTypeServiceImpl implements BenRelationshipTypeService { private BeneficiaryRelationshipTypeRepository beneficiaryRelationshipTypeRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java index b272cb05..32bb9565 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java @@ -31,8 +31,10 @@ import com.iemr.common.data.beneficiary.BeneficiaryOccupation; import com.iemr.common.repository.beneficiary.BeneficiaryOccupationRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BeneficiaryOccupationServiceImpl implements BeneficiaryOccupationService { private BeneficiaryOccupationRepository beneficiaryOccupationRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java index 7d5f1de0..82acb58f 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java @@ -60,12 +60,13 @@ import com.iemr.common.utils.validator.Validator; import jakarta.servlet.http.HttpServletRequest; - +import org.springframework.context.annotation.Profile; /** * @author WA875423 * */ @Service +@Profile("!swagger") public class RegisterBenificiaryServiceImpl implements RegisterBenificiaryService { @Autowired diff --git a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java index cd6d54c1..f910ced6 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java @@ -31,8 +31,9 @@ import com.iemr.common.data.beneficiary.SexualOrientation; import com.iemr.common.repository.userbeneficiarydata.SexualOrientationRepository; - +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class SexualOrientationServiceImpl implements SexualOrientationService { private SexualOrientationRepository sexualOrientationRepository; diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java index a6ddfbfe..c6ebe089 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java @@ -19,6 +19,7 @@ * 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.service.reportSecondary; import java.io.ByteArrayInputStream; diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java index 4107eb13..81a18611 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import com.iemr.common.data.callhandling.CallType; @@ -53,6 +54,7 @@ import com.iemr.common.utils.mapper.InputMapper; +@Profile("!swagger") @Service public class SecondaryReportServiceImpl implements SecondaryReportService { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/utils/FilterConfig.java b/src/main/java/com/iemr/common/utils/FilterConfig.java index 42bd04ad..9144a296 100644 --- a/src/main/java/com/iemr/common/utils/FilterConfig.java +++ b/src/main/java/com/iemr/common/utils/FilterConfig.java @@ -33,6 +33,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; @Configuration +@org.springframework.context.annotation.Profile("!swagger") public class FilterConfig { private static final Logger log = LoggerFactory.getLogger(FilterConfig.class); diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 1e9f589d..381f64de 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; @Component +@org.springframework.context.annotation.Profile("!swagger") public class JwtAuthenticationUtil { @Autowired diff --git a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java index 66f2777d..6c68f26d 100644 --- a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java +++ b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java @@ -29,6 +29,7 @@ import com.iemr.common.utils.mapper.OutputMapper; import jakarta.persistence.Column; +import org.springframework.context.annotation.Profile; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -36,6 +37,7 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; +@Profile("!swagger") @Entity @Table(name = "fact_bencall", schema = "db_reporting") public class SecondaryCallReport implements Serializable diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties new file mode 100644 index 00000000..f09e3c2b --- /dev/null +++ b/src/main/resources/application-swagger.properties @@ -0,0 +1,28 @@ +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:9090,http://localhost:8080} +# ---- Embedded DB for Swagger documentation generation +spring.datasource.url=jdbc:h2:mem:swaggerdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=none +spring.jpa.show-sql=false + +spring.sql.init.mode=never + +# Use placeholders for sensitive values +jwt.secret=JWT_SECRET +jwt.expiration=3600000 +sms-password= +sms-username= +start-grievancedatasync-scheduler=false +sms-consent-source-address= +send-message-url=http://localhost:8080/sms/sendMessage +secondary.datasource.username= +secondary.datasource.password= +secondary.datasource.url=jdbc:h2:mem:reportingdb +secondary.datasource.driver-class-name=org.h2.Driver + +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18723465..b8cdef3f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -371,6 +371,4 @@ video-call-url = allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent -sms-template-name = otp_consent - - +sms-template-name = otp_consent \ No newline at end of file From 3c1ef4b1d29a010e9211f9e9775035c740b66ba7 Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:35:33 +0530 Subject: [PATCH 38/70] Update the swagger json github workflow (#359) * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * fix(swagger): update the workflow and fix the running issue * fix(swagger): fix the swagger json workflow * chore(swagger): add fixed branch name in workflow * chore(ci): prevent multiple swagger sync PRs by using fixed branch * chore(swagger): add Dev/UAT/Demo servers to OpenAPI config * chore(swagger): avoid default server URLs * chore(swagger): remove field injection and inject URLs into OpenAPI bean --- .github/workflows/swagger-json.yml | 52 +++++++++++++------ .../com/iemr/common/config/SwaggerConfig.java | 26 +++++++--- .../resources/application-swagger.properties | 6 ++- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index bac1707d..413643f7 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -22,8 +22,8 @@ jobs: cache: maven - name: Build API (skip tests) - run: mvn clean package -DskipTests - + run: mvn -B clean package -DskipTests + - name: Install jq run: sudo apt-get update && sudo apt-get install -y jq @@ -37,19 +37,27 @@ jobs: - name: Wait for API & fetch Swagger run: | - for i in {1..30}; do + for i in {1..40}; do CODE=$(curl --connect-timeout 2 --max-time 5 -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + if [ "$CODE" = "200" ]; then - if jq . swagger_raw.json > common-api.json; then - echo "Swagger generated successfully" - exit 0 - else - echo "Failed to parse swagger_raw.json with jq" + jq . swagger_raw.json > common-api.json || { + echo "Swagger JSON invalid" + cat swagger_raw.json + exit 1 + } + + if [ "$(jq '.paths | length' common-api.json)" -eq 0 ]; then + echo "Swagger paths empty – failing" exit 1 fi + + echo "Swagger generated successfully" + exit 0 fi + echo "Waiting for API... ($i)" - sleep 5 + sleep 4 done echo "Swagger not generated" @@ -59,9 +67,17 @@ jobs: - name: Stop API if: always() run: | + # Graceful shutdown of the process group + sleep 5 + # Force kill the process group if still running if [ -f api_pid.txt ]; then - kill $(cat api_pid.txt) || true - fi + PID=$(cat api_pid.txt) + kill -TERM -- -"$PID" 2>/dev/null || true + sleep 2 + kill -9 -- -"$PID" 2>/dev/null || true + fi + # Fallback: kill any remaining java process on port 9090 + fuser -k 9090/tcp 2>/dev/null || true - name: Checkout AMRIT-Docs uses: actions/checkout@v4 @@ -69,21 +85,25 @@ jobs: repository: PSMRI/AMRIT-Docs token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs + fetch-depth: 0 - name: Copy Swagger JSON run: | mkdir -p amrit-docs/docs/swagger cp common-api.json amrit-docs/docs/swagger/common-api.json + # Use a fixed branch name for PRs to avoid accumulating stale PRs. + # This ensures only one open PR is updated per run; delete-branch: true cleans up after merge. - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - branch: auto/swagger-update-${{ github.run_id }} + branch: auto/swagger-update-common-api base: main - commit-message: Auto-update Common-API swagger - title: Auto-update Common-API swagger + commit-message: "chore(docs): auto-update Common-API swagger" + title: "chore(docs): auto-update Common-API swagger" + delete-branch: true body: | - This PR automatically updates the Common-API Swagger JSON + This PR automatically updates Common-API Swagger JSON from the latest main branch build. diff --git a/src/main/java/com/iemr/common/config/SwaggerConfig.java b/src/main/java/com/iemr/common/config/SwaggerConfig.java index 793f3a25..04bcec21 100644 --- a/src/main/java/com/iemr/common/config/SwaggerConfig.java +++ b/src/main/java/com/iemr/common/config/SwaggerConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -11,14 +12,23 @@ @Configuration public class SwaggerConfig { - - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI().info(new - Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) - .addSecurityItem(new SecurityRequirement().addList("my security")) - .components(new Components().addSecuritySchemes("my security", - new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))); + private static final String DEFAULT_SERVER_URL = "http://localhost:9090"; + + @Bean + public OpenAPI customOpenAPI(Environment env) { + String devUrl = env.getProperty("api.dev.url", DEFAULT_SERVER_URL); + String uatUrl = env.getProperty("api.uat.url", DEFAULT_SERVER_URL); + String demoUrl = env.getProperty("api.demo.url", DEFAULT_SERVER_URL); + return new OpenAPI() + .info(new Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) + .addSecurityItem(new SecurityRequirement().addList("my security")) + .components(new Components().addSecuritySchemes("my security", + new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))) + .servers(java.util.Arrays.asList( + new io.swagger.v3.oas.models.servers.Server().url(devUrl).description("Dev"), + new io.swagger.v3.oas.models.servers.Server().url(uatUrl).description("UAT"), + new io.swagger.v3.oas.models.servers.Server().url(demoUrl).description("Demo") + )); } } diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index f09e3c2b..f73e6fd2 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -25,4 +25,8 @@ secondary.datasource.url=jdbc:h2:mem:reportingdb secondary.datasource.driver-class-name=org.h2.Driver springdoc.api-docs.enabled=true -springdoc.swagger-ui.enabled=true \ No newline at end of file +springdoc.swagger-ui.enabled=true + +api.dev.url=${API_DEV_URL:https://amritwprdev.piramalswasthya.org} +api.uat.url=${API_UAT_URL:https://uatamrit.piramalswasthya.org} +api.demo.url=${API_DEMO_URL:https://amritdemo.piramalswasthya.org} \ No newline at end of file From a12c8adce059c78d1047c6f14d09f29798686dc1 Mon Sep 17 00:00:00 2001 From: Vaishnav Bhosale Date: Sun, 22 Feb 2026 11:21:59 +0530 Subject: [PATCH 39/70] Add /health endpoint and standardize /version response (#331) * Add /health endpoint and standardize /version response * Add license headers and Javadocs to health and version controllers * Enhance /health endpoint to check Database and Redis connectivity * Improve /health endpoint HTTP status handling and logging * Enhance database health check with validation query * Refactor health controller to constructor injection and constants * Refactor: Extract business logic to HealthService to keep controller lean * Refactor: Extract business logic to HealthService to keep controller lean * Fix: Use ObjectProvider for optional health dependencies --- .../controller/health/HealthController.java | 66 ++++++++++ .../controller/version/VersionController.java | 98 ++++++++------- .../controller/version/VersionInfo.java | 46 +++++++ .../common/service/health/HealthService.java | 114 ++++++++++++++++++ 4 files changed, 273 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/iemr/common/controller/health/HealthController.java create mode 100644 src/main/java/com/iemr/common/controller/version/VersionInfo.java create mode 100644 src/main/java/com/iemr/common/service/health/HealthService.java diff --git a/src/main/java/com/iemr/common/controller/health/HealthController.java b/src/main/java/com/iemr/common/controller/health/HealthController.java new file mode 100644 index 00000000..abfb536d --- /dev/null +++ b/src/main/java/com/iemr/common/controller/health/HealthController.java @@ -0,0 +1,66 @@ +/* + * 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.health; + +import com.iemr.common.service.health.HealthService; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Health check controller for Common-API. + * Verifies application liveness and dependency health (DB, Redis). + * + * @author vaishnavbhosale + */ +@RestController +public class HealthController { + + private static final Logger logger = LoggerFactory.getLogger(HealthController.class); + + private final HealthService healthService; + + public HealthController(HealthService healthService) { + this.healthService = healthService; + } + + @GetMapping("/health") + public ResponseEntity> health() { + logger.info("Health check endpoint called"); + + + Map healthStatus = healthService.checkHealth(); + + // Standard HTTP Status logic + String status = (String) healthStatus.get("status"); + HttpStatus httpStatus = "UP".equals(status) ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; + + logger.info("Health check completed with status: {}", status); + + return ResponseEntity.status(httpStatus).body(healthStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 705fccdc..10645866 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -1,30 +1,37 @@ /* -* 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/. -*/ + * 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/. + */ +/** + * REST controller exposing application version and build metadata. + *

+ * Provides the /version endpoint which returns the + * Git commit hash and build timestamp in a standardized JSON format. + *

+ * + * @author Vaishnav Bhosale + */ package com.iemr.common.controller.version; -import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; +import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,46 +39,35 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import com.iemr.common.utils.response.OutputResponse; - import io.swagger.v3.oas.annotations.Operation; - @RestController public class VersionController { - private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + private final Logger logger = + LoggerFactory.getLogger(this.getClass().getSimpleName()); @Operation(summary = "Get version") @RequestMapping(value = "/version", method = { RequestMethod.GET }) - public String versionInformation() { - OutputResponse output = new OutputResponse(); - try { - logger.info("version Controller Start"); - output.setResponse(readGitProperties()); - } catch (Exception e) { - output.setError(e); - } - - logger.info("version Controller End"); - return output.toString(); - } + public VersionInfo versionInformation() { - private String readGitProperties() throws Exception { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream inputStream = classLoader.getResourceAsStream("git.properties"); + Properties properties = new Properties(); - return readFromInputStream(inputStream); - } + try (InputStream is = getClass() + .getClassLoader() + .getResourceAsStream("git.properties")) { - private String readFromInputStream(InputStream inputStream) throws IOException { - StringBuilder resultStringBuilder = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) { - String line; - while ((line = br.readLine()) != null) { - resultStringBuilder.append(line).append("\n"); + if (is != null) { + properties.load(is); } + + } catch (Exception e) { + logger.error("Error reading git.properties", e); } - return resultStringBuilder.toString(); + + return new VersionInfo( + properties.getProperty("git.commit.id.abbrev", "unknown"), + properties.getProperty("git.build.time", "unknown") + ); } } diff --git a/src/main/java/com/iemr/common/controller/version/VersionInfo.java b/src/main/java/com/iemr/common/controller/version/VersionInfo.java new file mode 100644 index 00000000..20f560a1 --- /dev/null +++ b/src/main/java/com/iemr/common/controller/version/VersionInfo.java @@ -0,0 +1,46 @@ +/* + * 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/. + */ +/** + * DTO for exposing build and version metadata. + * + * @author vaishnavbhosale + */ +package com.iemr.common.controller.version; + +public class VersionInfo { + + private String commitHash; + private String buildTime; + + public VersionInfo(String commitHash, String buildTime) { + this.commitHash = commitHash; + this.buildTime = buildTime; + } + + public String getCommitHash() { + return commitHash; + } + + public String getBuildTime() { + return buildTime; + } +} diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java new file mode 100644 index 00000000..1e312a09 --- /dev/null +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -0,0 +1,114 @@ +/* + * 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.service.health; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; // <--- VITAL IMPORT +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.HashMap; +import java.util.Map; + +/** + * Health check service for Common-API. + * Verifies application liveness and dependency health (DB, Redis). + * + * @author vaishnavbhosale + */ +@Service +public class HealthService { + + private static final Logger logger = LoggerFactory.getLogger(HealthService.class); + + private static final String COMPONENT_DATABASE = "database"; + private static final String COMPONENT_REDIS = "redis"; + + private final DataSource dataSource; + private final RedisConnectionFactory redisConnectionFactory; + + // --- CORRECT CONSTRUCTOR START --- + public HealthService(ObjectProvider dataSourceProvider, + ObjectProvider redisProvider) { + // This allows them to be null without crashing the app + this.dataSource = dataSourceProvider.getIfAvailable(); + this.redisConnectionFactory = redisProvider.getIfAvailable(); + } + // --- CORRECT CONSTRUCTOR END --- + + public Map checkHealth() { + Map response = new HashMap<>(); + Map components = new HashMap<>(); + + boolean dbUp = checkDatabase(components); + boolean redisUp = checkRedis(components); + + boolean overallUp = dbUp && redisUp; + + response.put("status", overallUp ? "UP" : "DOWN"); + response.put("components", components); + + return response; + } + + private boolean checkDatabase(Map components) { + if (dataSource == null) { + components.put(COMPONENT_DATABASE, "NOT_CONFIGURED"); + return true; + } + + try (Connection connection = dataSource.getConnection(); + var statement = connection.createStatement()) { + + statement.execute("SELECT 1"); + components.put(COMPONENT_DATABASE, "UP"); + return true; + + } catch (Exception e) { + logger.error("Database health check failed", e); + components.put(COMPONENT_DATABASE, "DOWN"); + return false; + } + } + + private boolean checkRedis(Map components) { + if (redisConnectionFactory == null) { + components.put(COMPONENT_REDIS, "NOT_CONFIGURED"); + return true; + } + + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + connection.ping(); + components.put(COMPONENT_REDIS, "UP"); + return true; + + } catch (Exception e) { + logger.error("Redis health check failed", e); + components.put(COMPONENT_REDIS, "DOWN"); + return false; + } + } +} \ No newline at end of file From 4b440457411fc5d1a2b38ee4ff798685c73a62f1 Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:14:33 +0530 Subject: [PATCH 40/70] Add advance health check for database (#361) * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * fix(swagger): update the workflow and fix the running issue * fix(swagger): fix the swagger json workflow * chore(swagger): add fixed branch name in workflow * chore(ci): prevent multiple swagger sync PRs by using fixed branch * chore(swagger): add Dev/UAT/Demo servers to OpenAPI config * chore(swagger): avoid default server URLs * chore(swagger): remove field injection and inject URLs into OpenAPI bean * feat(health,version): update version and health endpoints and add advance check for database * fix(health): normalize severity and fix slow query false positives * fix(health): avoid false CRITICAL on single long-running MySQL transaction * fix(health): enforce 3s DB connection timeout via HikariCP --- pom.xml | 26 ++ .../controller/version/VersionController.java | 57 ++- .../common/service/health/HealthService.java | 391 ++++++++++++++++-- .../utils/JwtUserIdValidationFilter.java | 4 +- 4 files changed, 415 insertions(+), 63 deletions(-) diff --git a/pom.xml b/pom.xml index f818762c..f0badf34 100644 --- a/pom.xml +++ b/pom.xml @@ -526,6 +526,32 @@ ${artifactId}-${version} + + io.github.git-commit-id + git-commit-id-maven-plugin + 9.0.2 + + + get-the-git-infos + + revision + + initialize + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.branch$ + ^git.commit.id.abbrev$ + ^git.build.version$ + ^git.build.time$ + + false + false + + org.apache.maven.plugins maven-jar-plugin diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 10645866..a6e6d828 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -22,8 +22,8 @@ /** * REST controller exposing application version and build metadata. *

- * Provides the /version endpoint which returns the - * Git commit hash and build timestamp in a standardized JSON format. + * Provides the /version endpoint which returns Git metadata + * in a standardized JSON format consistent across all AMRIT APIs. *

* * @author Vaishnav Bhosale @@ -31,12 +31,16 @@ package com.iemr.common.controller.version; import java.io.InputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; @@ -46,28 +50,39 @@ public class VersionController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final String UNKNOWN_VALUE = "unknown"; - @Operation(summary = "Get version") - @RequestMapping(value = "/version", method = { RequestMethod.GET }) - public VersionInfo versionInformation() { + @Operation(summary = "Get version information") + @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> versionInformation() { + Map response = new LinkedHashMap<>(); + try { + logger.info("version Controller Start"); + Properties gitProperties = loadGitProperties(); + response.put("buildTimestamp", gitProperties.getProperty("git.build.time", UNKNOWN_VALUE)); + response.put("version", gitProperties.getProperty("git.build.version", UNKNOWN_VALUE)); + response.put("branch", gitProperties.getProperty("git.branch", UNKNOWN_VALUE)); + response.put("commitHash", gitProperties.getProperty("git.commit.id.abbrev", UNKNOWN_VALUE)); + } catch (Exception e) { + logger.error("Failed to load version information", e); + response.put("buildTimestamp", UNKNOWN_VALUE); + response.put("version", UNKNOWN_VALUE); + response.put("branch", UNKNOWN_VALUE); + response.put("commitHash", UNKNOWN_VALUE); + } + logger.info("version Controller End"); + return ResponseEntity.ok(response); + } + private Properties loadGitProperties() throws IOException { Properties properties = new Properties(); - - try (InputStream is = getClass() - .getClassLoader() + try (InputStream input = getClass().getClassLoader() .getResourceAsStream("git.properties")) { - - if (is != null) { - properties.load(is); + if (input != null) { + properties.load(input); } - - } catch (Exception e) { - logger.error("Error reading git.properties", e); } - - return new VersionInfo( - properties.getProperty("git.commit.id.abbrev", "unknown"), - properties.getProperty("git.build.time", "unknown") - ); + return properties; } } diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 1e312a09..7714efce 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -23,92 +23,401 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectProvider; // <--- VITAL IMPORT +import org.springframework.beans.factory.ObjectProvider; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; +import jakarta.annotation.PreDestroy; import javax.sql.DataSource; import java.sql.Connection; -import java.util.HashMap; +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.Instant; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; -/** - * Health check service for Common-API. - * Verifies application liveness and dependency health (DB, Redis). - * - * @author vaishnavbhosale - */ @Service public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); - private static final String COMPONENT_DATABASE = "database"; - private static final String COMPONENT_REDIS = "redis"; + // Event log constants + private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; + private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; + private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + + // Response field constants + private static final String FIELD_STATUS = "status"; + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_MYSQL = "mysql"; + private static final String FIELD_REDIS = "redis"; + private static final String FIELD_CHECKED_AT = "checkedAt"; + + // Severity constants + private static final String SEVERITY_CRITICAL = "CRITICAL"; + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_INFO = "INFO"; + // Database query constants + private static final String STATUS_VALUE = "Value"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; + + // Thresholds + private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW + private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING + private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int LONG_TXN_WARNING_THRESHOLD = 1; // ≥1 long txn → WARNING + private static final int LONG_TXN_CRITICAL_THRESHOLD = 5; // ≥5 long txns → CRITICAL + private static final int LONG_TXN_SECONDS = 60; // transaction age threshold + private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING + private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL + private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval + private static final long DIAGNOSTIC_GUARD_SEC = 25; // safety dedup guard private final DataSource dataSource; private final RedisConnectionFactory redisConnectionFactory; - // --- CORRECT CONSTRUCTOR START --- + private final ScheduledExecutorService diagnosticScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "mysql-diagnostic-thread"); + t.setDaemon(true); + return t; + }); + + private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); + private final AtomicReference cachedDbSeverity = + new AtomicReference<>(SEVERITY_OK); + private final AtomicLong previousDeadlockCount = new AtomicLong(0); + private final AtomicLong previousSlowQueryCount = new AtomicLong(0); public HealthService(ObjectProvider dataSourceProvider, ObjectProvider redisProvider) { - // This allows them to be null without crashing the app this.dataSource = dataSourceProvider.getIfAvailable(); this.redisConnectionFactory = redisProvider.getIfAvailable(); + + // Start background diagnostics only if DB is configured. + // Initial delay = 0 so the first run happens at startup. + if (this.dataSource != null) { + diagnosticScheduler.scheduleAtFixedRate( + this::runAdvancedMySQLDiagnostics, + 0, + DIAGNOSTIC_INTERVAL_SEC, + TimeUnit.SECONDS + ); + } } - // --- CORRECT CONSTRUCTOR END --- + @PreDestroy + public void shutdownDiagnostics() { + logger.info("[HEALTH_SERVICE_SHUTDOWN] Shutting down diagnostic scheduler..."); + diagnosticScheduler.shutdown(); + try { + if (!diagnosticScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler did not terminate gracefully"); + diagnosticScheduler.shutdownNow(); + } + logger.info("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler shut down successfully"); + } catch (InterruptedException e) { + logger.error("[HEALTH_SERVICE_SHUTDOWN] Interrupted while shutting down scheduler", e); + diagnosticScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // PUBLIC — Called by the /health controller public Map checkHealth() { - Map response = new HashMap<>(); - Map components = new HashMap<>(); + Map response = new LinkedHashMap<>(); + + Map mysqlResult = checkDatabaseConnectivity(); + Map redisResult = checkRedisConnectivity(); - boolean dbUp = checkDatabase(components); - boolean redisUp = checkRedis(components); + String mysqlStatus = (String) mysqlResult.get(FIELD_STATUS); + String redisStatus = (String) redisResult.get(FIELD_STATUS); - boolean overallUp = dbUp && redisUp; + boolean overallUp = !STATUS_DOWN.equals(mysqlStatus) && !STATUS_DOWN.equals(redisStatus); - response.put("status", overallUp ? "UP" : "DOWN"); - response.put("components", components); + response.put(FIELD_STATUS, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(FIELD_CHECKED_AT, Instant.now().toString()); + + // Expose only status and severity, keep diagnostics internal + Map mysqlSummary = new LinkedHashMap<>(); + mysqlSummary.put(FIELD_STATUS, mysqlResult.get(FIELD_STATUS)); + mysqlSummary.put(FIELD_SEVERITY, mysqlResult.get(FIELD_SEVERITY)); + + Map redisSummary = new LinkedHashMap<>(); + redisSummary.put(FIELD_STATUS, redisResult.get(FIELD_STATUS)); + redisSummary.put(FIELD_SEVERITY, redisResult.get(FIELD_SEVERITY)); + + response.put(FIELD_MYSQL, mysqlSummary); + response.put(FIELD_REDIS, redisSummary); return response; } + // Runs only SELECT 1 with a hard 3-second timeout on query execution. + // NOTE: getConnection() is NOT bounded by this timeout — it respects the pool's + // connectionTimeout (default 30s in HikariCP). For true 3-second /health guarantees, + // configure the DataSource connectionTimeout ≤ 3 seconds or wrap in an ExecutorService timeout. + private Map checkDatabaseConnectivity() { + Map result = new LinkedHashMap<>(); - private boolean checkDatabase(Map components) { if (dataSource == null) { - components.put(COMPONENT_DATABASE, "NOT_CONFIGURED"); - return true; + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; } - try (Connection connection = dataSource.getConnection(); - var statement = connection.createStatement()) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { - statement.execute("SELECT 1"); - components.put(COMPONENT_DATABASE, "UP"); - return true; + stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution + stmt.execute("SELECT 1"); + + // If SELECT 1 succeeds, use cached severity from background diagnostics + String severity = cachedDbSeverity.get(); + result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_SEVERITY, severity); } catch (Exception e) { - logger.error("Database health check failed", e); - components.put(COMPONENT_DATABASE, "DOWN"); - return false; + // Log connection failure as a structured event + logger.error( + "[MYSQL_CONNECT_FAILED] MySQL connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } + + return result; } - private boolean checkRedis(Map components) { + private Map checkRedisConnectivity() { + Map result = new LinkedHashMap<>(); + if (redisConnectionFactory == null) { - components.put(COMPONENT_REDIS, "NOT_CONFIGURED"); - return true; + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; + } + + try (RedisConnection conn = redisConnectionFactory.getConnection()) { + conn.ping(); + result.put(FIELD_STATUS, STATUS_UP); + result.put(FIELD_SEVERITY, SEVERITY_OK); + + } catch (Exception e) { + logger.error( + "[REDIS_CONNECT_FAILED] Redis connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); + } + + return result; + } + + private void runAdvancedMySQLDiagnostics() { + // Dedup guard: skip if last run was within the past 25 seconds + long now = System.currentTimeMillis(); + if (now - lastDiagnosticRunAt.get() < TimeUnit.SECONDS.toMillis(DIAGNOSTIC_GUARD_SEC)) { + return; } + lastDiagnosticRunAt.set(now); + + String worstSeverity = SEVERITY_OK; - try (RedisConnection connection = redisConnectionFactory.getConnection()) { - connection.ping(); - components.put(COMPONENT_REDIS, "UP"); - return true; + try (Connection conn = dataSource.getConnection()) { + worstSeverity = escalate(worstSeverity, performStuckProcessCheck(conn)); + worstSeverity = escalate(worstSeverity, performLongTransactionCheck(conn)); + worstSeverity = escalate(worstSeverity, performDeadlockCheck(conn)); + worstSeverity = escalate(worstSeverity, performSlowQueryCheck(conn)); + worstSeverity = escalate(worstSeverity, performConnectionUsageCheck(conn)); } catch (Exception e) { - logger.error("Redis health check failed", e); - components.put(COMPONENT_REDIS, "DOWN"); - return false; + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = SEVERITY_CRITICAL; } + + cachedDbSeverity.set(worstSeverity); + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); + } + + private String performStuckProcessCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { + + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + logger.warn( + "[{}] Stuck MySQL processes detected above threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + return SEVERITY_WARNING; + } else { + logger.info( + "[{}] Stuck MySQL processes below threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + } + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performLongTransactionCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + LONG_TXN_SECONDS)) { + + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount >= LONG_TXN_WARNING_THRESHOLD) { + logger.warn( + "[{}] InnoDB long-running transaction(s) detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, LONG_TXN_SECONDS + ); + // Graduated escalation: WARNING for 1-4, CRITICAL for 5+ + return lockCount >= LONG_TXN_CRITICAL_THRESHOLD + ? SEVERITY_CRITICAL : SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performDeadlockCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { + + if (rs.next()) { + long currentDeadlocks = rs.getLong(STATUS_VALUE); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); + + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performSlowQueryCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong(STATUS_VALUE); + long previousSlow = previousSlowQueryCount.getAndSet(slowQueries); + + // Only warn if slow queries have *increased* since last run + if (slowQueries > previousSlow) { + long delta = slowQueries - previousSlow; + logger.warn( + "[{}] New slow queries detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, delta, slowQueries + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + + private String performConnectionUsageCheck(Connection conn) { + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; + + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt(STATUS_VALUE); + } + + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt(STATUS_VALUE); + } + + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_CRITICAL; + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_WARNING; + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } + private String resolveDatabaseStatus(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> STATUS_DOWN; + case SEVERITY_WARNING -> STATUS_DEGRADED; + default -> STATUS_UP; + }; + } + private String escalate(String current, String candidate) { + return severityRank(candidate) > severityRank(current) ? candidate : current; + } + + private int severityRank(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> 2; + case SEVERITY_WARNING -> 1; + default -> 0; + }; } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 81d79221..364aa12d 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -251,7 +251,9 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/userLogout") || path.startsWith(contextPath + "/user/validateSecurityQuestionAndAnswer") || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") - || path.startsWith(contextPath + "/user/refreshToken"); + || path.startsWith(contextPath + "/user/refreshToken") + || path.equals(contextPath + "/health") + || path.equals(contextPath + "/version"); } private String getJwtTokenFromCookies(HttpServletRequest request) { From 231773e7ff2f25330e9cfbe9ea16decd2e31cf26 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:02:38 +0530 Subject: [PATCH 41/70] Merge Release-3.8.0 (3.6.1) to Main (#379) * Move code to 3.6.1 to 3.8.0 (#372) * fix: cors spell fixes and import of packages updates * fix: deployment issue fix * feat: amm-1959 dhis token for cho report re-direction * fix: beneficiary history on revisit (#320) * fix: call type mapper (#322) * Elasticsearch implementation for Beneficiary Search (#324) * fix: implement functionality to search beneficiaries with Elasticsearch * fix: remove unwanted import * fix: update pom.xml * fix: change the response code * variable added * Elastic Search Implementation for Advanced Search (#327) * fix: cherry-pick commits for advanced search * fix: cherry-pick commit for token issue - mobile application * fix: add the missing properties * fix: add function to retrieve userid * fix: move the fetch Userid to jwtUtil * fix:signature check for mmu * fix: retrive any user without deleted * fix: update KM filepath * FLW-713 Remove All File Upload Options (#350) * FLW-713 Remove All File Upload Options * Fix UserServiceRoleRepo dependency issue and codeRabit comment * fixed coderabit comment * fix userMappingId issue * Add SMS functionality in release-3.6.1 (#358) * Enable SMS Functionality in MMU App to Send Prescriptions (#325) * fix: sms template save and map mmu (#306) * Vb/sms (#307) * fix: sms template save and map mmu * fix: enable mms for mmu prescription * Enable SMS Functionality in MMU App to Send Prescriptions (#325) * fix: sms template save and map mmu (#306) * Vb/sms (#307) * fix: sms template save and map mmu * fix: enable mms for mmu prescription --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> --------- Co-authored-by: 5Amogh Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: vanitha1822 Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> * fix: add OTP rate limiting to prevent OTP flooding on sendConsent endpoint (#373) - Add OtpRateLimiterService with Redis-backed per-mobile rate limits (3/min, 10/hr, 20/day) - Add OtpRateLimitException for 429 responses - Integrate rate limiter in BeneficiaryOTPHandlerImpl and BeneficiaryConsentController - Add otp.ratelimit.* properties to common_ci and common_docker profiles - Update common_example.properties with new OTP rate limit config Co-authored-by: Claude Sonnet 4.6 * Health api (#376) * Cherry-pick health and version API enhancements to release-3.6.1 (#371) * feat(health,version): update version and health endpoints and add advance check for database * fix(health): normalize severity and fix slow query false positives * fix(health): avoid false CRITICAL on single long-running MySQL transaction * fix(health): enforce 3s DB connection timeout via HikariCP * Release 3.6.1 (#374) * feat(health,version): update version and health endpoints and add advance check for database * fix(health): normalize severity and fix slow query false positives * fix(health): avoid false CRITICAL on single long-running MySQL transaction * fix(health): enforce 3s DB connection timeout via HikariCP * feat(health): add healthcontroller and fix versioncontroller issues * fix: build error (#375) --------- Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> --- .vscode/settings.json | 3 + pom.xml | 2 +- src/main/environment/common_ci.properties | 5 + src/main/environment/common_docker.properties | 6 +- .../environment/common_example.properties | 7 + .../BeneficiaryRegistrationController.java | 88 +++++ .../BeneficiaryConsentController.java | 8 +- .../dynamicForm/DynamicFormController.java | 4 +- .../controller/users/IEMRAdminController.java | 20 +- .../controller/version/VersionController.java | 2 +- .../common/data/dynamic_from/FormField.java | 8 + .../common/data/translation/Translation.java | 2 + .../common/data/users/UserServiceRole.java | 364 ++++++++++++++++++ .../dto/dynamicForm/FieldResponseDTO.java | 2 + .../exception/OtpRateLimitException.java | 30 ++ .../users/IEMRUserRepositoryCustom.java | 3 + .../repository/users/UserServiceRoleRepo.java | 12 + .../beneficiary/IEMRSearchUserService.java | 4 + .../IEMRSearchUserServiceImpl.java | 99 ++++- .../IdentityBeneficiaryService.java | 7 + .../IdentityBeneficiaryServiceImpl.java | 210 ++++++++-- .../BeneficiaryOTPHandlerImpl.java | 5 + .../dynamicForm/FormMasterService.java | 4 +- .../dynamicForm/FormMasterServiceImpl.java | 206 ++++++---- .../KMFileManagerServiceImpl.java | 4 +- .../service/otp/OtpRateLimiterService.java | 104 +++++ .../users/EmployeeSignatureServiceImpl.java | 6 +- .../service/users/IEMRAdminUserService.java | 2 + .../users/IEMRAdminUserServiceImpl.java | 6 + .../java/com/iemr/common/utils/JwtUtil.java | 55 +++ .../iemr/common/utils/RestTemplateUtil.java | 7 +- .../utils/http/HTTPRequestInterceptor.java | 177 +++++---- src/main/resources/application.properties | 4 +- 33 files changed, 1233 insertions(+), 233 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/main/java/com/iemr/common/data/users/UserServiceRole.java create mode 100644 src/main/java/com/iemr/common/exception/OtpRateLimitException.java create mode 100644 src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java create mode 100644 src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b016a89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index f0badf34..3250086b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.0 + 3.8.0 war Common-API diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 0184b32f..f2b774a3 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -19,6 +19,7 @@ km-base-path=@env.KM_API_BASE_PATH@ km-root-path=/okm:personal/users/ km-guest-user=@env.KM_GUEST_USER@ km-guest-password=@env.KM_GUEST_PASSWORD@ +tempFilePath=@env.TEMP_FILE_PATH@ # CTI Config cti-server-ip=@env.CTI_SERVER_IP@ @@ -202,5 +203,9 @@ platform.feedback.ratelimit.day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT platform.feedback.ratelimit.user-day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT@ platform.feedback.ratelimit.fail-window-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES@ platform.feedback.ratelimit.backoff-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES@ +otp.ratelimit.enabled=@env.OTP_RATELIMIT_ENABLED@ +otp.ratelimit.minute-limit=@env.OTP_RATELIMIT_MINUTE_LIMIT@ +otp.ratelimit.hour-limit=@env.OTP_RATELIMIT_HOUR_LIMIT@ +otp.ratelimit.day-limit=@env.OTP_RATELIMIT_DAY_LIMIT@ generateBeneficiaryIDs-api-url=@env.GEN_BENEFICIARY_IDS_API_URL@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index a81ea62e..a5c633e4 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -126,7 +126,7 @@ everwellRegisterBenficiary = ${COMMON_API_BASE_URL}/beneficiary/create ## LungAssessment credentials lungAssessmentEmail = ${SWAASA_EMAIL} lungAssessmentPassword =${SWAASA_PASSWORD} - +tempFilePath=${TEMP_FILE_PATH} ## SWASSA APIs lungAssessmentAdminLogin = ${SWAASA_BASE_URL}/api/adminLogin @@ -206,4 +206,8 @@ platform.feedback.ratelimit.day-limit=${PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT} platform.feedback.ratelimit.user-day-limit=${PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT} platform.feedback.ratelimit.fail-window-minutes=${PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES} platform.feedback.ratelimit.backoff-minutes=${PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES} +otp.ratelimit.enabled=${OTP_RATELIMIT_ENABLED} +otp.ratelimit.minute-limit=${OTP_RATELIMIT_MINUTE_LIMIT} +otp.ratelimit.hour-limit=${OTP_RATELIMIT_HOUR_LIMIT} +otp.ratelimit.day-limit=${OTP_RATELIMIT_DAY_LIMIT} generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index aca73ddb..e3b5c031 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -25,6 +25,8 @@ km-root-path=/okm:personal/users/ km-guest-user=guest km-guest-password=guest +tempFilePath=/opt/openkm + # CTI Config cti-server-ip=10.208.122.99 cti-logger_base_url=http://10.208.122.99/logger @@ -224,5 +226,10 @@ platform.feedback.ratelimit.user-day-limit=50 platform.feedback.ratelimit.fail-window-minutes=5 platform.feedback.ratelimit.backoff-minutes=15 +# --- OTP Rate Limiting (per mobile number) --- +otp.ratelimit.minute-limit=3 +otp.ratelimit.hour-limit=10 +otp.ratelimit.day-limit=20 + ### generate Beneficiary IDs URL generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 3d9e204e..67f57981 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -71,6 +72,8 @@ import com.iemr.common.service.userbeneficiarydata.MaritalStatusService; import com.iemr.common.service.userbeneficiarydata.StatusService; import com.iemr.common.service.userbeneficiarydata.TitleService; +import com.iemr.common.utils.CookieUtil; +import com.iemr.common.utils.JwtUtil; import com.iemr.common.utils.mapper.InputMapper; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -105,6 +108,8 @@ public class BeneficiaryRegistrationController { private BeneficiaryOccupationService beneficiaryOccupationService; private GovtIdentityTypeService govtIdentityTypeService; + @Autowired + private JwtUtil jwtUtil; @Autowired public void setBenRelationshipTypeService(BenRelationshipTypeService benRelationshipTypeService) { @@ -344,6 +349,54 @@ public String searchUserByPhone( return response.toString(); } + @Operation(summary = "Provide the list of beneficiaries using Elasticsearch") + @RequestMapping(value = "/searchUser", method = RequestMethod.POST, headers = "Authorization") + public String searchUser(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + logger.info("Universal search request received"); + + JsonParser parser = new JsonParser(); + JsonObject requestObj = parser.parse(request).getAsJsonObject(); + + String searchQuery = null; + if (requestObj.has("search") && !requestObj.get("search").isJsonNull()) { + searchQuery = requestObj.get("search").getAsString(); + } + + if (searchQuery == null || searchQuery.trim().isEmpty()) { + response.setError(400, "Search query is required"); + return response.toString(); + } + + String auth = httpRequest.getHeader("Authorization"); + + Integer userID = jwtUtil.getUserIdFromRequest(httpRequest); + + logger.info("ES search for userId: {}", userID); + + Boolean is1097 = false; + if (requestObj.has("is1097") && !requestObj.get("is1097").isJsonNull()) { + is1097 = requestObj.get("is1097").getAsBoolean(); + } + + logger.info("Searching with query: {}, userId: {}, is1097: {}", searchQuery, userID, is1097); + String result = iemrSearchUserService.searchUser(searchQuery, userID, auth, is1097); + + if (result == null || result.trim().isEmpty()) { + response.setError(200, "No beneficiaries found"); + return response.toString(); + } + + return result; + + } catch (Exception e) { + logger.error("Error in universal search: {}", e.getMessage(), e); + response.setError(400, "Error searching beneficiaries: " + e.getMessage()); + return response.toString(); + } + } + @Operation(summary = "Provide the list of beneficiaries based on search criteria") @RequestMapping(value = "/searchBeneficiary", method = RequestMethod.POST, headers = "Authorization") public String searchBeneficiary( @@ -366,6 +419,41 @@ public String searchBeneficiary( return output.toString(); } + /** + * Elasticsearch-based advanced search endpoint + */ + @Operation(summary = "Advanced search beneficiaries using Elasticsearch") + @RequestMapping(value = "/searchBeneficiaryES", method = RequestMethod.POST, headers = "Authorization") + public String searchBeneficiaryES( + @RequestBody BeneficiaryModel request, + HttpServletRequest httpRequest) { + + logger.info("searchBeneficiaryES request: {}", request); + OutputResponse output = new OutputResponse(); + + try { + + String auth = httpRequest.getHeader("Authorization"); + + Integer userID = jwtUtil.getUserIdFromRequest(httpRequest); + + logger.info("ES Advanced search for userId: {}", userID); + + String result = iemrSearchUserService.findBeneficiaryES(request, userID, auth); + + return result; + + } catch (NumberFormatException ne) { + logger.error("searchBeneficiaryES failed with number format error: {}", ne.getMessage(), ne); + output.setError(400, "Invalid number format in search criteria"); + return output.toString(); + } catch (Exception e) { + logger.error("searchBeneficiaryES failed with error: {}", e.getMessage(), e); + output.setError(500, "Error searching beneficiaries: " + e.getMessage()); + return output.toString(); + } + } + @Operation(summary = "Provide all common data list needed for beneficiary registration") @RequestMapping(value = "/getRegistrationData", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") public String getRegistrationData() { diff --git a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java index 77492d89..8750c0a1 100644 --- a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java +++ b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java @@ -22,6 +22,7 @@ package com.iemr.common.controller.beneficiaryConsent; import com.iemr.common.data.beneficiaryConsent.BeneficiaryConsentRequest; +import com.iemr.common.exception.OtpRateLimitException; import com.iemr.common.service.beneficiaryOTPHandler.BeneficiaryOTPHandler; import com.iemr.common.utils.mapper.InputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -58,7 +59,9 @@ public String sendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBody logger.info(success.toString()); response.setResponse(success); - + } catch (OtpRateLimitException e) { + logger.warn("OTP rate limit hit for sendConsent: " + e.getMessage()); + response.setError(429, e.getMessage()); } catch (Exception e) { response.setError(500, "error : " + e); } @@ -105,6 +108,9 @@ public String resendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBod else response.setError(500, "failure"); + } catch (OtpRateLimitException e) { + logger.warn("OTP rate limit hit for resendConsent: " + e.getMessage()); + response.setError(429, e.getMessage()); } catch (Exception e) { logger.error("error in re-sending Consent : " + e); response.setError(500, "error : " + e); diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index 30a1bc3f..62bf7e7c 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -84,9 +84,9 @@ public ResponseEntity> deleteField(@PathVariable Long fieldId) { } @GetMapping(value = "form/{formId}/fields") - public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang) { + public ResponseEntity> getStructuredForm(@PathVariable String formId, @RequestParam(name = "lang", defaultValue = "en") String lang,@RequestHeader(value = "jwttoken") String token) { try { - Object result = formMasterService.getStructuredFormByFormId(formId,lang); + Object result = formMasterService.getStructuredFormByFormId(formId,lang,token); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success("Form structure fetched successfully", HttpStatus.OK.value(), result)); } catch (Exception e) { 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 8bc0e74d..554500f3 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -1224,7 +1224,25 @@ public ResponseEntity getUserDetails(@PathVariable("userName") String userNam return new ResponseEntity<>(Map.of("error", "UserName Not Found"), HttpStatus.NOT_FOUND); } User user = users.get(0); - return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), HttpStatus.OK); + return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), + HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(Map.of("error", "Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR); + } + + } + + @Operation(summary = "Get UserId based on userName") + @GetMapping(value = "/checkUserName/{userName}", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public ResponseEntity checkUserDetails(@PathVariable("userName") String userName) { + try { + List users = iemrAdminUserServiceImpl.findUserIdByUserName(userName); + if (users.isEmpty()) { + return new ResponseEntity<>(Map.of("error", "UserName Not Found"), HttpStatus.NOT_FOUND); + } + User user = users.get(0); + return new ResponseEntity<>(Map.of("userName", user.getUserName(), "userId", user.getUserID()), + HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(Map.of("error", "Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index a6e6d828..1b02ee59 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -85,4 +85,4 @@ private Properties loadGitProperties() throws IOException { } return properties; } -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java index 39785ae9..1b195db9 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormField.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormField.java @@ -53,7 +53,15 @@ public class FormField { @Column(name = "sequence") private Integer sequence; + @Column(name = "is_editable") + private Boolean isEditable; + + @Column(name = "state_code") + private Integer stateCode; + @Column(name = "created_at") private LocalDateTime createdAt = LocalDateTime.now(); + + } 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 81a906fa..0dad116d 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -18,6 +18,8 @@ public class Translation { private String english; @Column(name = "hindi_translation") private String hindiTranslation; + @Column(name = "assamese_translation") + private String assameseTranslation; @Column(name = "is_active") private Boolean isActive; } diff --git a/src/main/java/com/iemr/common/data/users/UserServiceRole.java b/src/main/java/com/iemr/common/data/users/UserServiceRole.java new file mode 100644 index 00000000..935940d5 --- /dev/null +++ b/src/main/java/com/iemr/common/data/users/UserServiceRole.java @@ -0,0 +1,364 @@ +package com.iemr.common.data.users; + +import jakarta.persistence.*; +import java.util.Objects; + +@Entity +@Table(name = "v_userservicerolemapping", schema = "db_iemr") +public class UserServiceRole { + private Integer userId; + private int usrMappingId; + private String name; + private String userName; + private Short serviceId; + private String serviceName; + private Boolean isNational; + private Integer stateId; + private String stateName; + private Integer workingDistrictId; + private String workingDistrictName; + private Integer workingLocationId; + private Short serviceProviderId; + private String locationName; + private String workingLocationAddress; + private Integer roleId; + private String roleName; + private Integer providerServiceMapId; + private String agentId; + private Short psmStatusId; + private String psmStatus; + private Boolean userServciceRoleDeleted; + private Boolean userDeleted; + private Boolean serviceProviderDeleted; + private Boolean roleDeleted; + private Boolean providerServiceMappingDeleted; + private Boolean isInbound; + private Boolean isOutbound; + private Integer blockid; + private String blockname; + private String villageid; + private String villagename; + + @Basic + @Column(name = "UserID") + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + @Basic + @Column(name = "USRMappingID") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public int getUsrMappingId() { + return usrMappingId; + } + + public void setUsrMappingId(int usrMappingId) { + this.usrMappingId = usrMappingId; + } + + @Basic + @Column(name = "Name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Basic + @Column(name = "UserName") + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @Basic + @Column(name = "ServiceID") + public Short getServiceId() { + return serviceId; + } + + public void setServiceId(Short serviceId) { + this.serviceId = serviceId; + } + + @Basic + @Column(name = "ServiceName") + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + @Basic + @Column(name = "IsNational") + public Boolean getNational() { + return isNational; + } + + public void setNational(Boolean national) { + isNational = national; + } + + @Basic + @Column(name = "StateID") + public Integer getStateId() { + return stateId; + } + + public void setStateId(Integer stateId) { + this.stateId = stateId; + } + + @Basic + @Column(name = "StateName") + public String getStateName() { + return stateName; + } + + public void setStateName(String stateName) { + this.stateName = stateName; + } + + @Basic + @Column(name = "WorkingDistrictID") + public Integer getWorkingDistrictId() { + return workingDistrictId; + } + + public void setWorkingDistrictId(Integer workingDistrictId) { + this.workingDistrictId = workingDistrictId; + } + + @Basic + @Column(name = "WorkingDistrictName") + public String getWorkingDistrictName() { + return workingDistrictName; + } + + public void setWorkingDistrictName(String workingDistrictName) { + this.workingDistrictName = workingDistrictName; + } + + @Basic + @Column(name = "WorkingLocationID") + public Integer getWorkingLocationId() { + return workingLocationId; + } + + public void setWorkingLocationId(Integer workingLocationId) { + this.workingLocationId = workingLocationId; + } + + @Basic + @Column(name = "ServiceProviderID") + public Short getServiceProviderId() { + return serviceProviderId; + } + + public void setServiceProviderId(Short serviceProviderId) { + this.serviceProviderId = serviceProviderId; + } + + @Basic + @Column(name = "LocationName") + public String getLocationName() { + return locationName; + } + + public void setLocationName(String locationName) { + this.locationName = locationName; + } + + @Basic + @Column(name = "WorkingLocationAddress") + public String getWorkingLocationAddress() { + return workingLocationAddress; + } + + public void setWorkingLocationAddress(String workingLocationAddress) { + this.workingLocationAddress = workingLocationAddress; + } + + @Basic + @Column(name = "RoleID") + public Integer getRoleId() { + return roleId; + } + + public void setRoleId(Integer roleId) { + this.roleId = roleId; + } + + @Basic + @Column(name = "RoleName") + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + @Basic + @Column(name = "ProviderServiceMapID") + public Integer getProviderServiceMapId() { + return providerServiceMapId; + } + + public void setProviderServiceMapId(Integer providerServiceMapId) { + this.providerServiceMapId = providerServiceMapId; + } + + @Basic + @Column(name = "AgentID") + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + @Basic + @Column(name = "PSMStatusID") + public Short getPsmStatusId() { + return psmStatusId; + } + + public void setPsmStatusId(Short psmStatusId) { + this.psmStatusId = psmStatusId; + } + + @Basic + @Column(name = "PSMStatus") + public String getPsmStatus() { + return psmStatus; + } + + public void setPsmStatus(String psmStatus) { + this.psmStatus = psmStatus; + } + + @Basic + @Column(name = "UserServciceRoleDeleted") + public Boolean getUserServciceRoleDeleted() { + return userServciceRoleDeleted; + } + + public void setUserServciceRoleDeleted(Boolean userServciceRoleDeleted) { + this.userServciceRoleDeleted = userServciceRoleDeleted; + } + + @Basic + @Column(name = "UserDeleted") + public Boolean getUserDeleted() { + return userDeleted; + } + + public void setUserDeleted(Boolean userDeleted) { + this.userDeleted = userDeleted; + } + + @Basic + @Column(name = "ServiceProviderDeleted") + public Boolean getServiceProviderDeleted() { + return serviceProviderDeleted; + } + + public void setServiceProviderDeleted(Boolean serviceProviderDeleted) { + this.serviceProviderDeleted = serviceProviderDeleted; + } + + @Basic + @Column(name = "RoleDeleted") + public Boolean getRoleDeleted() { + return roleDeleted; + } + + public void setRoleDeleted(Boolean roleDeleted) { + this.roleDeleted = roleDeleted; + } + + @Basic + @Column(name = "ProviderServiceMappingDeleted") + public Boolean getProviderServiceMappingDeleted() { + return providerServiceMappingDeleted; + } + + public void setProviderServiceMappingDeleted(Boolean providerServiceMappingDeleted) { + this.providerServiceMappingDeleted = providerServiceMappingDeleted; + } + + @Basic + @Column(name = "isInbound") + public Boolean getInbound() { + return isInbound; + } + + public void setInbound(Boolean inbound) { + isInbound = inbound; + } + + @Basic + @Column(name = "isOutbound") + public Boolean getOutbound() { + return isOutbound; + } + + public void setOutbound(Boolean outbound) { + isOutbound = outbound; + } + + @Basic + @Column(name = "blockid") + public Integer getBlockid() { + return blockid; + } + + public void setBlockid(Integer blockid) { + this.blockid = blockid; + } + + @Basic + @Column(name = "blockname") + public String getBlockname() { + return blockname; + } + + public void setBlockname(String blockname) { + this.blockname = blockname; + } + + @Basic + @Column(name = "villageid") + public String getVillageid() { + return villageid; + } + + public void setVillageid(String villageid) { + this.villageid = villageid; + } + + @Basic + @Column(name = "villagename") + public String getVillagename() { + return villagename; + } + + public void setVillagename(String villagename) { + this.villagename = villagename; + } + +} diff --git a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java index 3415d91a..e41f8e80 100644 --- a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java +++ b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java @@ -18,6 +18,8 @@ public class FieldResponseDTO { private String defaultValue; private String placeholder; private Integer sequence; + private Boolean isEditable; + private Integer stateCode; private List options; private Map validation; private Map conditional; diff --git a/src/main/java/com/iemr/common/exception/OtpRateLimitException.java b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java new file mode 100644 index 00000000..a0f3b53f --- /dev/null +++ b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java @@ -0,0 +1,30 @@ +/* + * 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.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) +public class OtpRateLimitException extends RuntimeException { + public OtpRateLimitException(String message) { super(message); } +} 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 3ee48ab3..cc1abccc 100644 --- a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java @@ -78,4 +78,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID, User findByUserID(Long userID); + @Query("SELECT u FROM User u WHERE LOWER(u.userName) = LOWER(:userName)") + List findUserName(@Param("userName") String username); + } diff --git a/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java new file mode 100644 index 00000000..cfb85d0a --- /dev/null +++ b/src/main/java/com/iemr/common/repository/users/UserServiceRoleRepo.java @@ -0,0 +1,12 @@ +package com.iemr.common.repository.users; + +import com.iemr.common.data.users.UserServiceRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserServiceRoleRepo extends JpaRepository { + List findByUserName(String userName); +} diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java index 6e7848cd..e39cfcab 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserService.java @@ -38,6 +38,10 @@ String findByBeneficiaryPhoneNo(BenPhoneMap benPhoneMap, Integer pageNo, Integer String findBeneficiary(BeneficiaryModel request, String auth) throws Exception; + String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception; + + String findBeneficiaryES(BeneficiaryModel i_beneficiary, Integer userId, String auth) throws Exception; + List userExitsCheckWithId(String beneficiaryID, String auth, Boolean is1097) throws Exception; public List userExitsCheckWithHealthId_ABHAId(String healthID, String auth, Boolean is1097) diff --git a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java index f67d7815..28d664c8 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IEMRSearchUserServiceImpl.java @@ -25,6 +25,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import org.slf4j.Logger; @@ -76,7 +77,7 @@ import com.iemr.common.repository.userbeneficiarydata.MaritalStatusRepository; import com.iemr.common.repository.userbeneficiarydata.SexualOrientationRepository; import com.iemr.common.repository.userbeneficiarydata.TitleRepository; -import com.iemr.common.utils.mapper.OutputMapper; +import com.iemr.common.utils.exception.IEMRException; /** * @@ -198,7 +199,7 @@ private void addCreatedDateToOtherFields(BeneficiaryModel beneficiaryModel) { JsonNode otherFieldsNode = objectMapper.readTree(beneficiaryModel.getOtherFields()); // Convert createdDate to a string - String createdDateString = beneficiaryModel.getCreatedDate().toString(); + String createdDateString = beneficiaryModel.getCreatedDate().toString(); // Add createdDate to the JSON node ((ObjectNode) otherFieldsNode).put("createdDate", createdDateString); @@ -219,10 +220,10 @@ public List userExitsCheckWithHealthId_ABHAId(String healthID, List beneficiaryList = new ArrayList(); // search patient by ben id, call Identity API List listBen = null; - if(healthID.contains("@")) { + if (healthID.contains("@")) { listBen = identityBeneficiaryService.getBeneficiaryListByHealthID_ABHAAddress(healthID, auth, is1097); - }else { + } else { String healthIdNumber = getHealthId(healthID); listBen = identityBeneficiaryService.getBeneficiaryListByHealthIDNo_ABHAIDNo(healthIdNumber, auth, is1097); } @@ -232,6 +233,7 @@ public List userExitsCheckWithHealthId_ABHAId(String healthID, } return beneficiaryList; } + private String getHealthId(String healthID) { String healthIdNumber = null; if (null != healthID) { @@ -249,6 +251,7 @@ private String getHealthId(String healthID) { } return healthIdNumber; } + // search patient by healthidNo / ABHA Id No @Override public List userExitsCheckWithHealthIdNo_ABHAIdNo(String healthIDNo, String auth, Boolean is1097) @@ -322,6 +325,90 @@ private void setBeneficiaryGender(List iBeneficiary) { } + /** + * Universal search using Elasticsearch + */ + @Override + public String searchUser(String searchQuery, Integer userId, String auth, Boolean is1097) throws Exception { + + try { + if (searchQuery == null || searchQuery.trim().isEmpty()) { + throw new IEMRException("Search query is required"); + } + + logger.info("Universal search with query: {}, userId: {}", searchQuery, userId); + + Map response = identityBeneficiaryService.searchBeneficiariesUsingES( + searchQuery, userId, auth, is1097); + + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(response); + + } catch (Exception e) { + logger.error("Error in universal search", e); + throw new Exception("Error searching beneficiaries: " + e.getMessage(), e); + } + } + + /** + * Advanced search using Elasticsearch with multiple criteria + */ + + @Override + public String findBeneficiaryES( + BeneficiaryModel i_beneficiary, + Integer userId, + String auth) throws Exception { + + try { + IdentitySearchDTO identitySearchDTO = identityBenEditMapper.getidentitysearchModel(i_beneficiary); + + if (i_beneficiary.getDOB() != null) { + identitySearchDTO.setDob(i_beneficiary.getDOB()); + } + + if (i_beneficiary.getHouseHoldID() != null) { + identitySearchDTO.setHouseHoldID(i_beneficiary.getHouseHoldID()); + } + + if (i_beneficiary.getIsD2D() != null) { + identitySearchDTO.setIsD2D(i_beneficiary.getIsD2D()); + } + + if (i_beneficiary.getBenPhoneMaps() != null + && !i_beneficiary.getBenPhoneMaps().isEmpty()) { + identitySearchDTO.setContactNumber( + i_beneficiary.getBenPhoneMaps().get(0).getPhoneNo()); + } + + if (i_beneficiary.getBeneficiaryID() != null + && !i_beneficiary.getBeneficiaryID().isEmpty()) { + identitySearchDTO.setBeneficiaryId( + new BigInteger(i_beneficiary.getBeneficiaryID())); + } + + i_beneficiary.setIs1097(Boolean.TRUE.equals(i_beneficiary.getIs1097())); + + Gson gson = new GsonBuilder() + .setDateFormat("yyyy-MM-dd") + .create(); + + String requestJson = gson.toJson(identitySearchDTO); + + Map response = identityBeneficiaryService.searchBeneficiaryListES( + requestJson, + auth, + i_beneficiary.getIs1097()); + + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(response); + + } catch (Exception e) { + logger.error("Error in ES advance search", e); + throw new Exception("Error searching beneficiaries using ES", e); + } + } + // Advance search @Override public String findBeneficiary(BeneficiaryModel i_beneficiary, String auth) throws Exception { @@ -364,7 +451,7 @@ public String findBeneficiary(BeneficiaryModel i_beneficiary, String auth) throw + (beneficiaryList != null ? beneficiaryList.size() : "No Beneficiary Found")); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(beneficiaryList); - + } // get response mapper @@ -374,7 +461,7 @@ public List getBeneficiaryListFromMapper(List { BeneficiaryModel beneficiary = benCompleteMapper.benDetailForOutboundDTOToIBeneficiary(beneficiaryModel); - if(null != beneficiaryModel && null != beneficiaryModel.getBeneficiaryDetails()) { + if (null != beneficiaryModel && null != beneficiaryModel.getBeneficiaryDetails()) { beneficiary.setCommunityName(beneficiaryModel.getBeneficiaryDetails().getCommunity()); beneficiary.setReligion(beneficiaryModel.getBeneficiaryDetails().getReligion()); beneficiary.setReligionName(beneficiaryModel.getBeneficiaryDetails().getReligion()); diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java index 8b84bc8a..41a132b0 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryService.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -53,6 +54,8 @@ List getBeneficiaryListByBenRegID(Long benRegId, String auth, List searchBeneficiaryList(String identitySearchDTO, String auth, Boolean is1097) throws IEMRException; + public Map searchBeneficiaryListES(String identitySearchDTO, String auth, Boolean is1097) throws IEMRException ; + Integer editIdentityEditDTOCommunityorEducation(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException; @@ -69,4 +72,8 @@ public List getBeneficiaryListByFamilyId(String familyId, Stri public List getBeneficiaryListByGovId(String identity, String auth, Boolean is1097) throws IEMRException; + + public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097) throws IEMRException; + + } diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java index f9ca6c96..350f2527 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java @@ -21,15 +21,19 @@ */ package com.iemr.common.service.beneficiary; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import com.google.gson.*; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.iemr.common.dto.identity.BeneficiariesDTO; @@ -43,6 +47,12 @@ import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; +import org.springframework.beans.factory.annotation.Value; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + @Service public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryService { @@ -59,12 +69,10 @@ public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryServic private static final String IDENTITY_BASE_URL = "IDENTITY_BASE_URL"; @Value("${genben-api}") - private String BEN_GEN ; - + private String BEN_GEN; @Value("${generateBeneficiaryIDs-api-url}") - private String BEN_GEN_API_URL ; - + private String BEN_GEN_API_URL; @Override // public List getBeneficiaryListByIDs() {// search by regID @@ -88,13 +96,10 @@ public List getBeneficiaryListByIDs(HashSet benIdList, String } if (null != result) { JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -107,10 +112,79 @@ public List getBeneficiaryListByIDs(HashSet benIdList, String return listBenDetailForOutboundDTO; } + /** + * Call Identity API's Elasticsearch universal search + */ + @Override + public Map searchBeneficiariesUsingES(String query, Integer userId, String auth, Boolean is1097) + throws IEMRException { + + Map response = new HashMap<>(); + + try { + HashMap headers = new HashMap<>(); + if (auth != null && !auth.isEmpty()) { + headers.put("Authorization", auth); + } + + String baseUrl = ConfigProperties + .getPropertyByName("identity-api-url-searchByES") + .replace( + IDENTITY_BASE_URL, + (Boolean.TRUE.equals(is1097)) ? identity1097BaseURL : identityBaseURL + ); + + StringBuilder url = new StringBuilder(baseUrl) + .append("?query=").append(URLEncoder.encode(query, StandardCharsets.UTF_8)); + + if (userId != null) { + url.append("&userId=").append(userId); + } + + logger.info("Calling Identity ES search URL: {}", url); + + String result = httpUtils.get(url.toString()); + + if (result == null || result.isEmpty()) { + response.put("data", Collections.emptyList()); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + return response; + } + + ObjectMapper mapper = new ObjectMapper(); + + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + + JsonNode rootNode = mapper.readTree(result); + + if (rootNode.has("statusCode") && rootNode.get("statusCode").asInt() != 200) { + String errMsg = rootNode.has("errorMessage") + ? rootNode.get("errorMessage").asText() + : "Identity ES search failed"; + throw new IEMRException(errMsg); + } + + response.put("data", rootNode.path("data")); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + + return response; + + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error calling Identity ES search API", e); + throw new IEMRException("Error calling Identity ES search API"); + } + } + @Override public List getPartialBeneficiaryListByIDs(HashSet benIdList, String auth, Boolean is1097) throws IEMRException { - // TODO Auto-generated method stub List listBenDetailForOutboundDTO = new ArrayList<>(); JsonParser parser = new JsonParser(); @@ -130,13 +204,10 @@ public List getPartialBeneficiaryListByIDs(HashSet benI throw new IEMRException(identityResponse.getErrorMessage()); } JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -151,9 +222,9 @@ public List getPartialBeneficiaryListByIDs(HashSet benI // search beneficiaries by phone number public List getBeneficiaryListByPhone(String phoneNo, String auth, Boolean is1097) throws IEMRException { - logger.info("Phone no from getBeneficiaryListByPhone: " + phoneNo); - String cleanedPhoneNo = cleanPhoneNumber(phoneNo); - logger.info("Cleaned phone no: " + cleanedPhoneNo); + logger.info("Phone no from getBeneficiaryListByPhone: " + phoneNo); + String cleanedPhoneNo = cleanPhoneNumber(phoneNo); + logger.info("Cleaned phone no: " + cleanedPhoneNo); List listBenDetailForOutboundDTO = new ArrayList<>(); @@ -165,12 +236,13 @@ public List getBeneficiaryListByPhone(String phoneNo, String a if (auth != null) { header.put("Authorization", auth); } - - logger.info("Result="+(ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") + + logger.info("Result=" + (ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo); result = httpUtils.post((ConfigProperties.getPropertyByName("identity-api-url-getByPhoneNum") - .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo, "", header); + .replace(IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL))) + cleanedPhoneNo, "", + header); OutputResponse identityResponse = InputMapper.gson().fromJson(result, OutputResponse.class); if (identityResponse.getStatusCode() == OutputResponse.USERID_FAILURE) { @@ -191,22 +263,22 @@ public List getBeneficiaryListByPhone(String phoneNo, String a } private String cleanPhoneNumber(String phoneNumber) { - if (phoneNumber == null || phoneNumber.trim().isEmpty()) { - return phoneNumber; - } - - String cleaned = phoneNumber.trim(); - - // Remove +91 prefix - if (cleaned.startsWith("+91")) { - cleaned = cleaned.substring(3); - } - // Remove 91 prefix if it's a 12-digit number (91 + 10 digit mobile) - else if (cleaned.startsWith("91") && cleaned.length() == 12) { - cleaned = cleaned.substring(2); - } - - return cleaned.trim(); + if (phoneNumber == null || phoneNumber.trim().isEmpty()) { + return phoneNumber; + } + + String cleaned = phoneNumber.trim(); + + // Remove +91 prefix + if (cleaned.startsWith("+91")) { + cleaned = cleaned.substring(3); + } + // Remove 91 prefix if it's a 12-digit number (91 + 10 digit mobile) + else if (cleaned.startsWith("91") && cleaned.length() == 12) { + cleaned = cleaned.substring(2); + } + + return cleaned.trim(); } @Override @@ -453,7 +525,6 @@ public String getIdentityResponse(String request, String auth, Boolean is1097) t return result; } - public Integer editIdentityEditDTO(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException { JsonParser parser = new JsonParser(); @@ -498,13 +569,10 @@ public List searchBeneficiaryList(String identitySearchDTO, St IDENTITY_BASE_URL, (is1097 ? identity1097BaseURL : identityBaseURL)), identitySearchDTO, header); JsonObject responseObj = (JsonObject) parser.parse(result); - // JsonArray data = (JsonArray) parser.parse( JsonObject data1 = (JsonObject) responseObj.get("response"); String s = data1.get("data").getAsString(); JsonArray responseArray = parser.parse(s).getAsJsonArray(); - // String data="s"; - // JsonArray responseArray = (JsonArray) parser.parse(data); for (JsonElement jsonElement : responseArray) { @@ -516,6 +584,68 @@ public List searchBeneficiaryList(String identitySearchDTO, St return listBenDetailForOutboundDTO; } + @Override + public Map searchBeneficiaryListES(String identitySearchDTO, String auth, Boolean is1097) + throws IEMRException { + + Map response = new HashMap<>(); + + try { + HashMap headers = new HashMap<>(); + if (auth != null && !auth.isEmpty()) { + headers.put("Authorization", auth); + } + + String url = ConfigProperties + .getPropertyByName("identity-api-url-advancesearch-es") + .replace( + IDENTITY_BASE_URL, + Boolean.TRUE.equals(is1097) + ? identity1097BaseURL + : identityBaseURL); + + logger.info("Calling Identity ES Advance Search API"); + + String result = httpUtils.post(url, identitySearchDTO, headers); + + if (result == null || result.isEmpty()) { + response.put("data", Collections.emptyList()); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + return response; + } + + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + JsonNode rootNode = mapper.readTree(result); + + if (rootNode.has("statusCode") + && rootNode.get("statusCode").asInt() != 200) { + + String errMsg = rootNode.has("errorMessage") + ? rootNode.get("errorMessage").asText() + : "Identity ES advance search failed"; + + throw new IEMRException(errMsg); + } + + response.put("data", rootNode.path("data")); + response.put("statusCode", 200); + response.put("status", "Success"); + response.put("errorMessage", "Success"); + + return response; + + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error calling Identity ES advance search API", e); + throw new IEMRException("Error calling Identity ES advance search API", e); + } + } + @Override public Integer editIdentityEditDTOCommunityorEducation(IdentityEditDTO identityEditDTO, String auth, Boolean is1097) throws IEMRException { @@ -555,11 +685,11 @@ public List generateBeneficiaryIDs(String request, String a if (auth != null) { header.put("Authorization", auth); } - + logger.info("Request to generate ben IDs: " + request); logger.info("Generating ben IDs API URL: " + BEN_GEN + BEN_GEN_API_URL); result = httpUtils.post(BEN_GEN + BEN_GEN_API_URL, request, header); -logger.info("Response from generate ben IDs: " + result); + logger.info("Response from generate ben IDs: " + result); OutputResponse identityResponse = inputMapper.gson().fromJson(result, OutputResponse.class); if (identityResponse.getStatusCode() == OutputResponse.USERID_FAILURE) { diff --git a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java index 42e0acfe..3cc0a709 100644 --- a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java @@ -32,6 +32,7 @@ import com.iemr.common.repository.sms.SMSTemplateRepository; import com.iemr.common.repository.sms.SMSTypeRepository; import com.iemr.common.service.otp.OTPHandler; +import com.iemr.common.service.otp.OtpRateLimiterService; import com.iemr.common.service.users.IEMRAdminUserServiceImpl; import com.iemr.common.utils.config.ConfigProperties; import com.iemr.common.utils.http.HttpUtils; @@ -59,6 +60,8 @@ public class BeneficiaryOTPHandlerImpl implements BeneficiaryOTPHandler { HttpUtils httpUtils; @Autowired private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl; + @Autowired + private OtpRateLimiterService otpRateLimiterService; final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired @@ -107,6 +110,7 @@ public String load(String key) { */ @Override public String sendOTP(BeneficiaryConsentRequest obj) throws Exception { + otpRateLimiterService.checkRateLimit(obj.getMobNo()); int otp = generateOTP(obj.getMobNo()); return sendSMS(otp, obj); } @@ -141,6 +145,7 @@ public JSONObject validateOTP(BeneficiaryConsentRequest obj) throws Exception { */ @Override public String resendOTP(BeneficiaryConsentRequest obj) throws Exception { + otpRateLimiterService.checkRateLimit(obj.getMobNo()); int otp = generateOTP(obj.getMobNo()); return sendSMS(otp, obj); } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java index 6d22e59a..e00663b7 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterService.java @@ -15,8 +15,6 @@ public interface FormMasterService { FormDefinition createForm(FormDTO dto); List createField(List dto); FormField updateField(FieldDTO dto); - - FormResponseDTO getStructuredFormByFormId(String formId,String lang); - + FormResponseDTO getStructuredFormByFormId(String formId,String lang,String token); void deleteField(Long fieldId); } 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 df019de7..acbcb4e7 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -1,38 +1,48 @@ package com.iemr.common.service.dynamicForm; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.common.data.dynamic_from.FormDefinition; import com.iemr.common.data.dynamic_from.FormField; import com.iemr.common.data.dynamic_from.FormModule; import com.iemr.common.data.translation.Translation; +import com.iemr.common.data.users.UserServiceRole; import com.iemr.common.dto.dynamicForm.*; import com.iemr.common.repository.dynamic_form.FieldRepository; import com.iemr.common.repository.dynamic_form.FormRepository; import com.iemr.common.repository.dynamic_form.ModuleRepository; import com.iemr.common.repository.translation.TranslationRepo; +import com.iemr.common.repository.users.UserServiceRoleRepo; +import com.iemr.common.utils.JwtUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service public class FormMasterServiceImpl implements FormMasterService { + final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired private ModuleRepository moduleRepo; - @Autowired private FormRepository formRepo; - @Autowired private FieldRepository fieldRepo; + @Autowired + private FormRepository formRepo; + @Autowired + private FieldRepository fieldRepo; @Autowired private TranslationRepo translationRepo; + @Autowired + private UserServiceRoleRepo userServiceRoleRepo; + + @Autowired + private JwtUtil jwtUtil; + @Override public FormModule createModule(ModuleDTO dto) { FormModule module = new FormModule(); @@ -85,7 +95,7 @@ public List createField(List dtoList) { public FormField updateField(FieldDTO dto) { FormField field = fieldRepo.findById(dto.getId()) .orElseThrow(() -> new IllegalArgumentException("Field not found: " + dto.getId())); - field.setId(dto.getId()); + field.setId(dto.getId()); field.setSectionTitle(dto.getSectionTitle()); field.setLabel(dto.getLabel()); field.setType(dto.getType()); @@ -103,96 +113,124 @@ public FormField updateField(FieldDTO dto) { } @Override - public FormResponseDTO getStructuredFormByFormId(String formId,String lang) { - FormDefinition form = formRepo.findByFormId(formId) - .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); + public FormResponseDTO getStructuredFormByFormId(String formId, String lang, String token) { + Integer stateId = 0; + try { + String username = jwtUtil.getUsernameFromToken(token); - List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); - ObjectMapper objectMapper = new ObjectMapper(); + stateId = userServiceRoleRepo.findByUserName(username) + .stream() + .findFirst() + .map(UserServiceRole::getStateId) + .filter(Objects::nonNull) + .orElse(null); - List fieldDtos = fields.stream() - .map(field -> { - String labelKey = field.getFieldId(); // field label already contains label_key - Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) - .orElse(null); + FormDefinition form = formRepo.findByFormId(formId) + .orElseThrow(() -> new IllegalArgumentException("Invalid form ID")); - String translatedLabel = field.getLabel(); // fallback + List fields = fieldRepo.findByForm_FormIdOrderBySequenceAsc(formId); + ObjectMapper objectMapper = new ObjectMapper(); - if (t != null) { - if ("hi".equalsIgnoreCase(lang)) { - translatedLabel = t.getHindiTranslation(); - } else { - translatedLabel = t.getEnglish(); - } - } - - FieldResponseDTO dto = new FieldResponseDTO(); - dto.setId(field.getId()); - dto.setVisible(field.getIsVisible()); - dto.setFormId(field.getForm().getFormId()); - dto.setSectionTitle(field.getSectionTitle()); - dto.setFieldId(field.getFieldId()); - dto.setLabel(translatedLabel); - dto.setType(field.getType()); - dto.setIsRequired(field.getIsRequired()); - dto.setDefaultValue(field.getDefaultValue()); - dto.setPlaceholder(field.getPlaceholder()); - dto.setSequence(field.getSequence()); - - try { - // Handle options - if (field.getOptions() != null && !field.getOptions().isBlank()) { - JsonNode node = objectMapper.readTree(field.getOptions()); - List options = null; - if (node.isArray()) { - options = objectMapper.convertValue(node, new TypeReference<>() {}); - } else if (node.has("options")) { - options = objectMapper.convertValue(node.get("options"), new TypeReference<>() {}); - } - dto.setOptions(options == null || options.isEmpty() ? null : options); - } else { - dto.setOptions(null); - } + Integer finalStateId = stateId; + List fieldDtos = fields.stream().filter(formField -> (formField.getStateCode().equals(0) || formField.getStateCode().equals(finalStateId))) + .map(field -> { + String labelKey = field.getFieldId(); // field label already contains label_key - // Handle validation - if (field.getValidation() != null && !field.getValidation().isBlank()) { - Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() {}); - dto.setValidation(validation.isEmpty() ? null : validation); - } else { - dto.setValidation(null); - } + Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) + .orElse(null); + + String translatedLabel = field.getLabel(); // fallback + + if (t != null) { + if ("hi".equalsIgnoreCase(lang)) { + translatedLabel = t.getHindiTranslation(); + } else if ("as".equalsIgnoreCase(lang)) { + translatedLabel = t.getAssameseTranslation(); + } else if ("en".equalsIgnoreCase(lang)) { + translatedLabel = t.getEnglish(); - // Handle conditional - if (field.getConditional() != null && !field.getConditional().isBlank()) { - Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() {}); - dto.setConditional(conditional.isEmpty() ? null : conditional); - } else { - dto.setConditional(null); + } } - } catch (Exception e) { - System.err.println("JSON Parsing Error in field: " + field.getFieldId()); - throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); - } + FieldResponseDTO dto = new FieldResponseDTO(); + dto.setId(field.getId()); + dto.setIsEditable(field.getIsEditable()); + dto.setStateCode(field.getStateCode()); + dto.setVisible(field.getIsVisible()); + dto.setFormId(field.getForm().getFormId()); + dto.setSectionTitle(field.getSectionTitle()); + dto.setFieldId(field.getFieldId()); + dto.setLabel(translatedLabel); + dto.setType(field.getType()); + dto.setIsRequired(field.getIsRequired()); + dto.setDefaultValue(field.getDefaultValue()); + dto.setPlaceholder(field.getPlaceholder()); + dto.setSequence(field.getSequence()); + + try { + // Handle options + if (field.getOptions() != null && !field.getOptions().isBlank()) { + JsonNode node = objectMapper.readTree(field.getOptions()); + List options = null; + if (node.isArray()) { + options = objectMapper.convertValue(node, new TypeReference<>() { + }); + } else if (node.has("options")) { + options = objectMapper.convertValue(node.get("options"), new TypeReference<>() { + }); + } + dto.setOptions(options == null || options.isEmpty() ? null : options); + } else { + dto.setOptions(null); + } - return dto; - }) - .sorted(Comparator.comparing(FieldResponseDTO::getId)) - .collect(Collectors.toList()); + // Handle validation + if (field.getValidation() != null && !field.getValidation().isBlank()) { + Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() { + }); + dto.setValidation(validation.isEmpty() ? null : validation); + } else { + dto.setValidation(null); + } + // Handle conditional + if (field.getConditional() != null && !field.getConditional().isBlank()) { + Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() { + }); + dto.setConditional(conditional.isEmpty() ? null : conditional); + } else { + dto.setConditional(null); + } + } catch (Exception e) { - GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); - singleSection.setSectionTitle(singleSection.getSectionTitle()); // your custom section title - singleSection.setFields(fieldDtos); + System.err.println("JSON Parsing Error in field: " + field.getFieldId()); + throw new RuntimeException("Failed to parse JSON for field: " + field.getFieldId(), e); + } - FormResponseDTO response = new FormResponseDTO(); - response.setVersion(form.getVersion()); - response.setFormId(form.getFormId()); - response.setFormName(form.getFormName()); - response.setSections(List.of(singleSection)); + return dto; + }) + .sorted(Comparator.comparing(FieldResponseDTO::getId)) + .collect(Collectors.toList()); + + + GroupedFieldResponseDTO singleSection = new GroupedFieldResponseDTO(); + singleSection.setFields(fieldDtos); + singleSection.setSectionTitle( + Objects.requireNonNullElse(singleSection.getSectionTitle(), "Section Title") + ); + FormResponseDTO response = new FormResponseDTO(); + response.setVersion(form.getVersion()); + response.setFormId(form.getFormId()); + response.setFormName(form.getFormName()); + response.setSections(List.of(singleSection)); + return response; + + } catch (Exception e) { + logger.error("Exception while building form response", e); + throw new RuntimeException("Failed to build form structure"); + } - return response; } diff --git a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java index 4297022a..43c5c1f2 100644 --- a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java +++ b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java @@ -89,6 +89,9 @@ public void setSubCategoryRepository(SubCategoryRepository subCategoryRepository @Value("${allowed.file.extensions}") private String allowedFileExtensions; + @Value("${tempFilePath}") + private String tempFilePath; + @Override public String getKMFileLists(String request) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); @@ -183,7 +186,6 @@ private ArrayList addKMFile(Iterable kmFileManager .replace("}", "").replace("[", "").replace("]", "").replace("|", "").replace("\\", "") .replace(":", "").replace(";", "").replace("-", "").replace("_", "").replace("+", "") .replace("=", "").replace("\"", "").replace("'", "")); - String tempFilePath = ConfigProperties.getPropertyByName("tempFilePath"); newFile = new FileOutputStream(tempFilePath + "/" + kmFileManager.getFileName()); newFile.write(Base64.getDecoder().decode(kmFileManager.getFileContent())); newFile.flush(); diff --git a/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java new file mode 100644 index 00000000..da06a64b --- /dev/null +++ b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java @@ -0,0 +1,104 @@ +/* + * 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.service.otp; + +import com.iemr.common.exception.OtpRateLimitException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; + +/** + * Rate-limits OTP send/resend requests per mobile number using Redis counters. + * + * Limits (configurable via properties): + * otp.ratelimit.minute-limit – max OTPs per minute (default 3) + * otp.ratelimit.hour-limit – max OTPs per hour (default 10) + * otp.ratelimit.day-limit – max OTPs per day (default 20) + * + * Redis key pattern: + * rl:otp:min:{mobNo}:{minuteSlot} TTL 60 s + * rl:otp:hr:{mobNo}:{hourSlot} TTL 3600 s + * rl:otp:day:{mobNo}:{yyyyMMdd} TTL 86400 s + */ +@Component +public class OtpRateLimiterService { + + private final StringRedisTemplate redis; + + @Value("${otp.ratelimit.enabled:true}") + private boolean enabled; + + @Value("${otp.ratelimit.minute-limit:3}") + private int minuteLimit; + + @Value("${otp.ratelimit.hour-limit:10}") + private int hourLimit; + + @Value("${otp.ratelimit.day-limit:20}") + private int dayLimit; + + public OtpRateLimiterService(StringRedisTemplate redis) { + this.redis = redis; + } + + /** + * Checks all three rate-limit windows for the given mobile number. + * Throws {@link OtpRateLimitException} if any limit is exceeded. + * No-op when otp.ratelimit.enabled=false. + */ + public void checkRateLimit(String mobNo) { + if (!enabled) return; + String today = LocalDate.now(ZoneId.of("Asia/Kolkata")) + .toString().replaceAll("-", ""); // yyyyMMdd + long minuteSlot = System.currentTimeMillis() / 60_000L; + long hourSlot = System.currentTimeMillis() / 3_600_000L; + + String minKey = "rl:otp:min:" + mobNo + ":" + minuteSlot; + String hourKey = "rl:otp:hr:" + mobNo + ":" + hourSlot; + String dayKey = "rl:otp:day:" + mobNo + ":" + today; + + if (incrementWithExpire(minKey, 60L) > minuteLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + minuteLimit + " OTPs allowed per minute. Please try again later."); + } + if (incrementWithExpire(hourKey, 3600L) > hourLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + hourLimit + " OTPs allowed per hour. Please try again later."); + } + if (incrementWithExpire(dayKey, 86400L) > dayLimit) { + throw new OtpRateLimitException( + "OTP request limit exceeded. Maximum " + dayLimit + " OTPs allowed per day. Please try again tomorrow."); + } + } + + private long incrementWithExpire(String key, long ttlSeconds) { + Long value = redis.opsForValue().increment(key, 1L); + if (value != null && value == 1L) { + redis.expire(key, ttlSeconds, TimeUnit.SECONDS); + } + return value == null ? 0L : value; + } +} diff --git a/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java b/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java index 115970ed..eff5c8e4 100644 --- a/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/EmployeeSignatureServiceImpl.java @@ -33,8 +33,6 @@ public class EmployeeSignatureServiceImpl implements EmployeeSignatureService { @Autowired EmployeeSignatureRepo employeeSignatureRepo; - - @Override public EmployeeSignature fetchSignature(Long userSignID) { // TODO Auto-generated method stub @@ -44,12 +42,12 @@ public EmployeeSignature fetchSignature(Long userSignID) { @Override public EmployeeSignature fetchActiveSignature(Long userSignID) { // New method - fetches only non-deleted records - return employeeSignatureRepo.findOneByUserIDAndDeleted(userSignID, false); + return employeeSignatureRepo.findOneByUserID(userSignID); } public Boolean existSignature(Long userID) { // TODO Auto-generated method stub - return employeeSignatureRepo.countByUserIDAndSignatureNotNull(userID)>0; + return employeeSignatureRepo.countByUserIDAndSignatureNotNull(userID) > 0; } } 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 d7dc6e2e..26b7bb15 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -123,6 +123,8 @@ public List getUserServiceRoleMappingForProvider(Integ List getUserIdbyUserName(String userName) throws IEMRException; + List findUserIdByUserName(String userName) 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 44bd2247..71d72c97 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -1224,4 +1224,10 @@ public List getUserIdbyUserName(String userName) { return iEMRUserRepositoryCustom.findByUserName(userName); } + + @Override + public List findUserIdByUserName(String userName) { + + return iEMRUserRepositoryCustom.findUserName(userName); + } } diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index d8414968..5d37a990 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -2,6 +2,7 @@ import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -161,4 +162,58 @@ public String getUserIdFromToken(String token) { public long getRefreshTokenExpiration() { return REFRESH_EXPIRATION_TIME; } + + /** + * Extract user ID from JWT token in the request (checks header and cookie) + * @param request the HTTP request + * @return the user ID, or null if not found + */ +public Integer getUserIdFromRequest(HttpServletRequest request) { + try { + String jwtToken = request.getHeader("Jwttoken"); + String cookieToken = CookieUtil.getJwtTokenFromCookie(request); + + // Prefer header token, fallback to cookie + String token = (jwtToken != null && !jwtToken.isEmpty()) ? jwtToken : cookieToken; + + if (token == null || token.isEmpty()) { + return null; + } + + Claims claims = validateToken(token); + if (claims == null) { + return null; + } + + String userId = claims.get("userId", String.class); + return userId != null ? Integer.parseInt(userId) : null; + + } catch (Exception e) { + return null; + } +} + +/** + * Extract username from JWT token in the request (checks header and cookie) + * @param request the HTTP request + * @return the username, or null if not found + */ +public String getUsernameFromRequest(HttpServletRequest request) { + try { + String jwtToken = request.getHeader("Jwttoken"); + String cookieToken = CookieUtil.getJwtTokenFromCookie(request); + + String token = (jwtToken != null && !jwtToken.isEmpty()) ? jwtToken : cookieToken; + + if (token == null || token.isEmpty()) { + return null; + } + + Claims claims = validateToken(token); + return claims != null ? claims.getSubject() : null; + + } catch (Exception e) { + return null; + } +} } diff --git a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java index c8299fe7..4e4fa483 100644 --- a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java +++ b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java @@ -39,6 +39,8 @@ public static HttpEntity createRequestEntity(Object body, String authori headers.add(HttpHeaders.AUTHORIZATION, authorization); if (null != requestHeader.getHeader(Constants.JWT_TOKEN)) { headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); + headers.add(HttpHeaders.COOKIE, "Jwttoken=" + requestHeader.getHeader(Constants.JWT_TOKEN)); + } if (null != jwtTokenFromCookie) { headers.add(HttpHeaders.COOKIE, "Jwttoken=" + jwtTokenFromCookie); @@ -77,9 +79,10 @@ public static void getJwttokenFromHeaders(HttpHeaders headers) { if (null != jwtTokenFromCookie) { headers.add(HttpHeaders.COOKIE, Constants.JWT_TOKEN + "=" + jwtTokenFromCookie); } else if (null != requestHeader.getHeader(Constants.JWT_TOKEN)) { - headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); - } + headers.add(Constants.JWT_TOKEN, requestHeader.getHeader(Constants.JWT_TOKEN)); + headers.add(HttpHeaders.COOKIE, Constants.JWT_TOKEN + "=" + requestHeader.getHeader(Constants.JWT_TOKEN)); + } } } diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index 0c609839..b4aaad60 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -36,10 +36,14 @@ import com.iemr.common.utils.sessionobject.SessionObject; import com.iemr.common.utils.validator.Validator; +import com.iemr.common.utils.JwtUtil; +import io.jsonwebtoken.Claims; +import com.iemr.common.utils.CookieUtil; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + @Configuration @Component public class HTTPRequestInterceptor implements HandlerInterceptor { @@ -50,6 +54,9 @@ public class HTTPRequestInterceptor implements HandlerInterceptor { @Value("${cors.allowed-origins}") private String allowedOrigins; + @Autowired + private JwtUtil jwtUtil; + @Autowired public void setValidator(Validator validator) { this.validator = validator; @@ -67,100 +74,101 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons boolean status = true; logger.info("In info preHandle we are Intercepting the Request"); logger.debug("In preHandle we are Intercepting the Request"); - // String authorization = request.getHeader("Authorization"); + // String authorization = request.getHeader("Authorization"); String authorization = null; String preAuth = request.getHeader("Authorization"); - if(null != preAuth && preAuth.contains("Bearer ")) - authorization=preAuth.replace("Bearer ", ""); + if (null != preAuth && preAuth.contains("Bearer ")) + authorization = preAuth.replace("Bearer ", ""); else authorization = preAuth; - + if (authorization == null || authorization.isEmpty()) { - logger.info("Authorization header is null or empty. Skipping HTTPRequestInterceptor."); - return true; // Allow the request to proceed without validation - } - logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization + logger.info("Authorization header is null or empty. Skipping HTTPRequestInterceptor."); + return true; // Allow the request to proceed without validation + } + + logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization + " || method :: " + request.getMethod()); if (!request.getMethod().equalsIgnoreCase("OPTIONS")) { try { String[] requestURIParts = request.getRequestURI().split("/"); String requestAPI = requestURIParts[requestURIParts.length - 1]; switch (requestAPI) { - case "userAuthenticate": - case "superUserAuthenticate": - case "userAuthenticateNew": - case "userAuthenticateV1": - case "forgetPassword": - case "setForgetPassword": - case "changePassword": - case "saveUserSecurityQuesAns": - case "doAgentLogout": - case "userLogout": - case "swagger-ui.html": - case "index.html": - case "index.css": - case "swagger-initializer.js": - case "swagger-config": - case "swagger-ui-bundle.js": - case "swagger-ui.css": - case "ui": - case "swagger-ui-standalone-preset.js": - case "favicon-32x32.png": - case "favicon-16x16.png": - case "swagger-resources": - case "api-docs": - case "updateBenCallIdsInPhoneBlock": - case "userAuthenticateByEncryption": - case "sendOTP": - case "validateOTP": - case "resendOTP": - case "validateSecurityQuestionAndAnswer": - case "logOutUserFromConcurrentSession": - case "refreshToken": - break; - case "error": - status = false; - break; - default: - String remoteAddress = request.getHeader("X-FORWARDED-FOR"); - if (remoteAddress == null || remoteAddress.trim().length() == 0) { - remoteAddress = request.getRemoteAddr(); - } - validator.checkKeyExists(authorization, remoteAddress); - break; + case "userAuthenticate": + case "superUserAuthenticate": + case "userAuthenticateNew": + case "userAuthenticateV1": + case "forgetPassword": + case "setForgetPassword": + case "changePassword": + case "saveUserSecurityQuesAns": + case "doAgentLogout": + case "userLogout": + case "swagger-ui.html": + case "index.html": + case "index.css": + case "swagger-initializer.js": + case "swagger-config": + case "swagger-ui-bundle.js": + case "swagger-ui.css": + case "ui": + case "swagger-ui-standalone-preset.js": + case "favicon-32x32.png": + case "favicon-16x16.png": + case "swagger-resources": + case "api-docs": + case "updateBenCallIdsInPhoneBlock": + case "userAuthenticateByEncryption": + case "sendOTP": + case "validateOTP": + case "resendOTP": + case "validateSecurityQuestionAndAnswer": + case "logOutUserFromConcurrentSession": + case "refreshToken": + break; + case "error": + status = false; + break; + default: + String remoteAddress = request.getHeader("X-FORWARDED-FOR"); + if (remoteAddress == null || remoteAddress.trim().length() == 0) { + remoteAddress = request.getRemoteAddr(); + } + validator.checkKeyExists(authorization, remoteAddress); + break; } } catch (Exception e) { logger.error("Authorization failed: {}", e.getMessage(), e); - String errorMessage = e.getMessage(); - if (errorMessage == null || errorMessage.trim().isEmpty()) { - errorMessage = "Unauthorized access or session expired."; - } - - String jsonErrorResponse = "{" - + "\"status\": \"Unauthorized\"," - + "\"statusCode\": 401," - + "\"errorMessage\": \"" + errorMessage.replace("\"", "\\\"") + "\"" - + "}"; - - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 - response.setContentType(MediaType.APPLICATION_JSON); - - String origin = request.getHeader("Origin"); - if (origin != null && isOriginAllowed(origin)) { - response.setHeader("Access-Control-Allow-Origin", origin); - response.setHeader("Access-Control-Allow-Credentials", "true"); - } else if (origin != null) { - logger.warn("CORS headers NOT added for error response | Unauthorized origin: {}", origin); - } - - // Better to use getBytes().length for accurate byte size - byte[] responseBytes = jsonErrorResponse.getBytes(StandardCharsets.UTF_8); - response.setContentLength(responseBytes.length); - - ServletOutputStream out = response.getOutputStream(); - out.write(responseBytes); - out.flush(); + String errorMessage = e.getMessage(); + if (errorMessage == null || errorMessage.trim().isEmpty()) { + errorMessage = "Unauthorized access or session expired."; + } + + String jsonErrorResponse = "{" + + "\"status\": \"Unauthorized\"," + + "\"statusCode\": 401," + + "\"errorMessage\": \"" + errorMessage.replace("\"", "\\\"") + "\"" + + "}"; + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 + response.setContentType(MediaType.APPLICATION_JSON); + + String origin = request.getHeader("Origin"); + if (origin != null && isOriginAllowed(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } else if (origin != null) { + logger.warn("CORS headers NOT added for error response | Unauthorized origin: {}", origin); + } + + // Better to use getBytes().length for accurate byte size + byte[] responseBytes = jsonErrorResponse.getBytes(StandardCharsets.UTF_8); + response.setContentLength(responseBytes.length); + + ServletOutputStream out = response.getOutputStream(); + out.write(responseBytes); + out.flush(); status = false; } } @@ -172,15 +180,14 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response, throws Exception { try { logger.debug("In postHandle we are Intercepting the Request"); - // String authorization = request.getHeader("Authorization"); String authorization = null; String postAuth = request.getHeader("Authorization"); - if(null != postAuth && postAuth.contains("Bearer ")) - authorization=postAuth.replace("Bearer ", ""); + if (null != postAuth && postAuth.contains("Bearer ")) + authorization = postAuth.replace("Bearer ", ""); else authorization = postAuth; logger.debug("RequestURI::" + request.getRequestURI() + " || Authorization ::" + authorization); - + if (authorization != null && !authorization.equals("")) { sessionObject.updateSessionObject(authorization, sessionObject.getSessionObject(authorization)); } @@ -212,8 +219,10 @@ private boolean isOriginAllowed(String origin) { .anyMatch(pattern -> { String regex = pattern .replace(".", "\\.") - .replace("*", ".*"); + .replace("*", ".*"); return origin.matches(regex); }); } + + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b8cdef3f..fef088ff 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,8 +27,6 @@ BeneficiarySmsTemplate=Beneficiary UPTSU SMS ######Project specific settings future-days=7 -## Path where the files would be stored before uploading to KM server -tempFilePath=c:/temp/ ## swaasa file path lungAssessmentPath=c:/swaasa_audio/ @@ -110,6 +108,7 @@ iemr.extend.expiry.time.changePassword=true iemr.session.expiry.time.changePassword=600 identity-api-url-advancesearch =IDENTITY_BASE_URL/id/advanceSearch +identity-api-url-advancesearch-es =IDENTITY_BASE_URL/beneficiary/advancedSearchES identity-api-url-getByBenRegIdList =IDENTITY_BASE_URL/id/getByBenRegIdList identity-api-url-getByPartialBenRegIdList =IDENTITY_BASE_URL/id/getByPartialBenRegIdList identity-api-url-getByPhoneNum =IDENTITY_BASE_URL/id/getByPhoneNum?phoneNum= @@ -118,6 +117,7 @@ identity-api-url-getByBenRegId =IDENTITY_BASE_URL/id/getByBenRegId?benRegId= identity-api-url-benCreate =IDENTITY_BASE_URL/id/create identity-api-url-benEdit =IDENTITY_BASE_URL/id/edit identity-api-url-benEditEducationCommunity=IDENTITY_BASE_URL/id/editEducationOrCommunity +identity-api-url-searchByES=IDENTITY_BASE_URL/beneficiary/search identity-api-url-getByFamilyId=IDENTITY_BASE_URL/id/searchByFamilyId?familyId= identity-api-url-getByGovIdentity=IDENTITY_BASE_URL/id/searchByGovIdentity?identity= From 833f1574eb79fef4611b9c64c78023a2b3469b6b Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:05:19 +0530 Subject: [PATCH 42/70] fix: video consultation functionality --- src/main/environment/common_ci.properties | 4 +- .../environment/common_example.properties | 4 +- .../videocall/VideoCallController.java | 24 +++++++- .../data/videocall/VideoCallParameters.java | 22 +++++++ .../mapper/videocall/VideoCallMapper.java | 23 +++++++- .../model/videocall/UpdateCallRequest.java | 22 +++++++ .../model/videocall/UpdateCallResponse.java | 22 +++++++ .../model/videocall/VideoCallRequest.java | 22 +++++++ .../VideoCallParameterRepository.java | 22 +++++++ .../service/videocall/VideoCallService.java | 24 +++++++- .../videocall/VideoCallServiceImpl.java | 57 +++++++++---------- 11 files changed, 208 insertions(+), 38 deletions(-) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 0184b32f..6997035e 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -189,8 +189,8 @@ captcha.enable-captcha=@env.ENABLE_CAPTCHA@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ -video-call-url=@env.VIDEO_CALL_URL@ -jibri.output.path=@env.JIBRI_OUTPUT_PATH@ +# Jitsi configuration +videocall.url=@env.VIDEO_CALL_URL@ video.recording.path=@env.VIDEO_RECORDING_PATH@ platform.feedback.ratelimit.enabled=@env.PLATFORM_FEEDBACK_RATELIMIT_ENABLED@ diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index aca73ddb..f55f142b 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -198,8 +198,8 @@ grievanceAllocationRetryConfiguration=3 logging.path=logs/ logging.file.name=logs/common-api.log -video-call-url=https://vc.piramalswasthya.org/? -jibri.output.path=/srv/jibri/recordings +# Jitsi configuration +videocall.url=https://vc.piramalswasthya.org/? video.recording.path=/srv/recordings captcha.secret-key= diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index 8eb2a3ad..fdbd9575 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -1,3 +1,25 @@ +/* +* 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.videocall; import java.util.HashMap; @@ -10,7 +32,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,7 +40,6 @@ import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.service.videocall.VideoCallService; import com.iemr.common.utils.response.OutputResponse; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java index c9df2d87..48a7b8ae 100644 --- a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java +++ b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java @@ -1,3 +1,25 @@ +/* +* 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.data.videocall; import java.sql.Timestamp; diff --git a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java index 521d5921..7e9a8f12 100644 --- a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java +++ b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java @@ -1,8 +1,29 @@ +/* +* 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.mapper.videocall; import java.util.List; import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; import org.mapstruct.IterableMapping; import org.mapstruct.factory.Mappers; diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java index 343198b3..681d4b63 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java @@ -1,3 +1,25 @@ +/* +* 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.model.videocall; import lombok.Data; diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java index f01f46f5..44c01215 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java @@ -1,3 +1,25 @@ +/* +* 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.model.videocall; import java.sql.Timestamp; diff --git a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java index d8a61eee..64abc044 100644 --- a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java @@ -1,3 +1,25 @@ +/* +* 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.model.videocall; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java index 251b877a..99798da2 100644 --- a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java +++ b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java @@ -1,3 +1,25 @@ +/* +* 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.repository.videocall; import java.util.List; diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java index 9322050b..dd457688 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java @@ -1,5 +1,27 @@ +/* +* 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.service.videocall; -import com.iemr.common.utils.response.OutputResponse; + import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 9db1a771..8b61d525 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -1,3 +1,25 @@ +/* +* 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.service.videocall; import org.apache.commons.lang.RandomStringUtils; @@ -36,14 +58,14 @@ public class VideoCallServiceImpl implements VideoCallService { private String meetingLink; private boolean isLinkSent = false; - private String consultationStatus = "Not Initiated"; - @Value("${video-call-url}") + @Value("${videocall.url}") private String jitsiLink; public VideoCallServiceImpl() { - // this.jitsiLink = ConfigProperties.getPropertyByName("video-call-url"); - // logger.info("Jitsi Link fetched: " + this.jitsiLink); + // Default constructor + this.meetingLink = null; + this.isLinkSent = false; } @Override @@ -99,10 +121,7 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { if (updateCount > 0) { videoCall.setLinkUsed(true); videoCallRepository.save(videoCall); - - // if ("Completed".equalsIgnoreCase(requestEntity.getCallStatus())) { - // saveRecordingFile(videoCall.getMeetingLink()); - // } + } else { throw new Exception("Failed to update the call status"); } @@ -110,28 +129,6 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { return OutputMapper.gsonWithoutExposeRestriction() .toJson(videoCallMapper.videoCallToResponse(videoCall)); } -private void saveRecordingFile(String meetingLink) { - try { - // Configurable Jibri recording location - String jibriOutputDir = ConfigProperties.getPropertyByName("jibri.output.path"); // e.g., /srv/jibri/recordings - String saveDir = ConfigProperties.getPropertyByName("video.recording.path"); // e.g., /srv/recordings - - File jibriDir = new File(jibriOutputDir); - File[] matchingFiles = jibriDir.listFiles((dir, name) -> name.contains(meetingLink) && name.endsWith(".mp4")); - - if (matchingFiles != null && matchingFiles.length > 0) { - File recording = matchingFiles[0]; - Path targetPath = Paths.get(saveDir, meetingLink + ".mp4"); - - Files.copy(recording.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); - logger.info("Recording file saved: " + targetPath); - } else { - logger.warn("No matching recording file found for meeting: " + meetingLink); - } - } catch (IOException e) { - logger.error("Error saving recording file: ", e); - } -} } From 114dee96205521604638b7589615bb3f22d3fd80 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 24 Mar 2026 14:43:58 +0530 Subject: [PATCH 43/70] fix: pom version update --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 11ad9f37..7e7b29d8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.6.0 + 3.7.0 war Common-API From 8e9a650515cba30bdd1ea0276bd102a22d65acc9 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 30 Mar 2026 12:15:31 +0530 Subject: [PATCH 44/70] fix: add cti-server-ip --- src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java index 81d8953a..fccf54f5 100644 --- a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java +++ b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -96,6 +97,9 @@ public class CTIServiceImpl implements CTIService { @Autowired private IEMRCalltypeRepositoryImplCustom iemrCalltypeRepositoryImplCustom; + @Value("${cti-server-ip}") + private String ctiServerIp; + public CTIServiceImpl() { if (httpUtils == null) { httpUtils = new HttpUtils(); From dfa00ac2906ceaddf0ad1394f6530d960a3c7169 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 30 Mar 2026 13:24:24 +0530 Subject: [PATCH 45/70] fix: comment unwanted code --- .../common/service/cti/CTIServiceImpl.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java index fccf54f5..7e5de8e9 100644 --- a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java +++ b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java @@ -98,7 +98,7 @@ public class CTIServiceImpl implements CTIService { private IEMRCalltypeRepositoryImplCustom iemrCalltypeRepositoryImplCustom; @Value("${cti-server-ip}") - private String ctiServerIp; + private String serverURL; public CTIServiceImpl() { if (httpUtils == null) { @@ -118,7 +118,7 @@ public OutputResponse addUpdateAgentSkills(String request, String ipAddress) thr ObjectMapper objectMapper = new ObjectMapper(); logger.debug("addUpdateAgentSkills input is " + request); String ctiURI = ConfigProperties.getPropertyByName("add-update-agent-skills-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentSkills agentSkills = objectMapper.readValue(request, AgentSkills.class); String agentID = (agentSkills.getAgentID() != null) ? agentSkills.getAgentID() : ""; @@ -150,7 +150,7 @@ public OutputResponse getCampaignSkills(String request, String ipAddress) throws OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-campaign-skills-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CampaignSkills agentState = objectMapper.readValue(request, CampaignSkills.class); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("CAMPAIGN_NAME", @@ -177,7 +177,7 @@ public OutputResponse getAgentState(String request, String ipAddress) throws IEM OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-agent-status-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentState agentState = objectMapper.readValue(request, AgentState.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -207,7 +207,7 @@ public OutputResponse getAgentCallStats(String request, String ipAddress) throws OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-agent-call-stats-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentCallStats agentState = objectMapper.readValue(request, AgentCallStats.class); String agentID = (agentState.getAgentID() != null) ? agentState.getAgentID() : ""; @@ -239,7 +239,7 @@ public OutputResponse getCampaignNames(String request, String ipAddress) throws OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-campaign-name-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CampaignNames agentState = objectMapper.readValue(request, CampaignNames.class); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("SEARCH_KEY", (agentState.getServiceName() != null) ? agentState.getServiceName() : ""); @@ -266,7 +266,7 @@ public OutputResponse doAgentLogin(String request, String ipAddress) throws IEMR OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("do-agent-login-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentState agentState = objectMapper.readValue(request, AgentState.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -296,7 +296,7 @@ public OutputResponse getLoginKey(String request, String ipAddress) throws IEMRE OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-login-key-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentLoginKey agentState = objectMapper.readValue(request, AgentLoginKey.class); String decryptPassword = null; @@ -329,7 +329,7 @@ public OutputResponse agentLogout(String request, String ipAddress) throws IEMRE OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("do-agent-logout-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentState agentState = objectMapper.readValue(request, AgentState.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -361,7 +361,7 @@ public OutputResponse getOnlineAgents(String request, String ipAddress) throws I OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("do-online-agent-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); AgentState agentState = objectMapper.readValue(request, AgentState.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -392,7 +392,7 @@ public OutputResponse callBeneficiary(String request, String ipAddress) throws I OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("call-beneficiary-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CallBeneficiary agentState = objectMapper.readValue(request, CallBeneficiary.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -433,7 +433,7 @@ public OutputResponse addUpdateUserData(String request, String ipAddress) throws * SESSION_TIMEOUT&designation=DESIGNATION&resFormat=3 */ - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CTIUser ctiUser = objectMapper.readValue(request, CTIUser.class); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("USERNAME", (ctiUser.getUsername() != null) ? ctiUser.getUsername() : ""); @@ -465,7 +465,7 @@ public OutputResponse getTransferCampaigns(String request, String ipAddress) thr OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("fetch-transferrable-campaigns-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CTICampaigns agentState = objectMapper.readValue(request, CTICampaigns.class); String agentID = (agentState.getAgent_id() != null) ? agentState.getAgent_id() : ""; @@ -497,7 +497,7 @@ public OutputResponse getCampaignRoles(String request, String remoteAddr) throws OutputResponse output = new OutputResponse(); ObjectMapper objectMapper = new ObjectMapper(); String ctiURI = ConfigProperties.getPropertyByName("get-campaign-roles-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); CampaignRole campaign = objectMapper.readValue(request, CampaignRole.class); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("CAMPAIGN_NAME", (campaign.getCampaign() != null) ? campaign.getCampaign() : ""); @@ -529,7 +529,7 @@ public OutputResponse setCallDisposition(String request, String remoteAddr) thro String agentIP = !agentIPResp.equals(DEFAULT_IP) ? agentIPResp : remoteAddr; String ctiURI = ConfigProperties.getPropertyByName("update-call-disposition-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("AGENT_ID", agentID); ctiURI = ctiURI.replace("AGENT_IP", agentIP); @@ -557,7 +557,7 @@ public OutputResponse createVoiceFile(String request, String remoteAddr) throws ObjectMapper objectMapper = new ObjectMapper(); CTIVoiceFile disposition = objectMapper.readValue(request, CTIVoiceFile.class); String ctiURI = ConfigProperties.getPropertyByName("mix-voice-file-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("AGENT_ID", (disposition.getAgent_id() != null) ? disposition.getAgent_id() : ""); // ctiURI = ctiURI.replace("AGENT_IP", remoteAddr); @@ -582,7 +582,7 @@ public OutputResponse getVoiceFile(String request, String remoteAddr) throws IEM ObjectMapper objectMapper = new ObjectMapper(); CTIVoiceFile disposition = objectMapper.readValue(request, CTIVoiceFile.class); String ctiURI = ConfigProperties.getPropertyByName("get-voice-file-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("AGENT_ID", (disposition.getAgent_id() != null) ? disposition.getAgent_id() : ""); // ctiURI = ctiURI.replace("AGENT_IP", remoteAddr); @@ -608,7 +608,7 @@ public OutputResponse getVoiceFileNew(String request, String remoteAddr) throws OutputResponse output = new OutputResponse(); CTIVoiceFile disposition = InputMapper.gson().fromJson(request, CTIVoiceFile.class); String ctiURI = ConfigProperties.getPropertyByName("get-voice-file-URL-New"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("AGENT_ID", (disposition.getAgent_id() != null) ? disposition.getAgent_id() : ""); // ctiURI = ctiURI.replace("AGENT_IP", remoteAddr); @@ -975,7 +975,7 @@ public OutputResponse addAutoDialNumbers(String request, String ipAddress) throw OutputResponse result = new OutputResponse(); logger.debug("addUpdateAgentSkills input is " + request); String ctiURI = ConfigProperties.getPropertyByName("add-auto-dail-numbers-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ObjectMapper objectMapper = new ObjectMapper(); AutoPreviewDial[] autoPreviewDialArray = objectMapper.readValue(request, AutoPreviewDial[].class); @@ -1016,7 +1016,7 @@ public OutputResponse setAutoDialNumbers(String request, String ipAddress) throw OutputResponse result = new OutputResponse(); logger.debug("setAutoDialNumbers input is " + request); String ctiURI = ConfigProperties.getPropertyByName("set-auto-dail-numbers-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ObjectMapper objectMapper = new ObjectMapper(); AutoPreviewDial autoPreviewDial = objectMapper.readValue(request, AutoPreviewDial.class); @@ -1059,7 +1059,7 @@ public OutputResponse getIVRSPathDetails(String request, String remoteAddress) t OutputResponse result = new OutputResponse(); logger.debug("getZoneDetails input is " + request); String ctiURI = ConfigProperties.getPropertyByName("agent-ivrs-path-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ObjectMapper objectMapper = new ObjectMapper(); AgentState zoneData = objectMapper.readValue(request, AgentState.class); From 9f379fb14a8c83231d19fc429e35a777f977cf11 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 30 Mar 2026 14:31:34 +0530 Subject: [PATCH 46/70] fix: update videocall url property --- src/main/environment/common_docker.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index a81ea62e..12408a9a 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -192,7 +192,7 @@ firebase.enabled=${FIREBASE_ENABLE} firebase.credential-file=${FIREBASE_CREDENTIAL} -video-call-url=${VIDEO_CALL_URL} +videocall.url=${VIDEO_CALL_URL} jibri.output.path={JIBRI_OUTPUT_PATH} video.recording.path={VIDEO_RECORDING_PATH} From f0a1177a3c630a129a6d1f7075aeec50e5db3d13 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 30 Mar 2026 15:53:01 +0530 Subject: [PATCH 47/70] fix: update cti-server-ip --- .../service/nhm_dashboard/NHM_DashboardServiceImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java index 4448cc07..d7afd579 100644 --- a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java +++ b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.google.gson.Gson; @@ -61,6 +62,9 @@ public class NHM_DashboardServiceImpl implements NHM_DashboardService { @Autowired private DetailedCallReportRepo detailedCallReportRepo; + @Value("${cti-server-ip}") + private String serverURL; + public String pushAbandonCalls(AbandonCallSummary abandonCallSummary) throws Exception { logger.info("NHM_abandon call push API request : " + abandonCallSummary.toString()); @@ -227,7 +231,7 @@ public List callAgentSummaryReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-agent-summary-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); @@ -272,7 +276,7 @@ public List callDetailedCallReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-details-call-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); From eec7cfefbadbbb7c6c46b23964300695b3088199 Mon Sep 17 00:00:00 2001 From: SnehaRH Date: Mon, 30 Mar 2026 19:05:18 +0530 Subject: [PATCH 48/70] docs: add CLAUDE.md for Claude Code guidance Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 CLAUDE.md 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 From 4b5d2500e1756350cf1712e2ee134c5243fdc03c Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 31 Mar 2026 16:26:26 +0530 Subject: [PATCH 49/70] fix: KM issue --- .../utils/km/openkm/OpenKMServiceImpl.java | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) 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 2be04cfc..acb47e6f 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 @@ -51,38 +51,43 @@ import org.glassfish.jersey.client.JerseyClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +@Service public class OpenKMServiceImpl implements KMService { - // private ConfigProperties configProperties; - // - // @Autowired - // public void setConfigProperties(ConfigProperties configProperties) - // { - // this.configProperties = configProperties; - // } + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private static String url; - private static String username; - private static String password; - private static String kmRootPath; - private static String guestUser; - private static String guestPassword; + + @Value("${km-base-url}") + private String url; + + @Value("${km-username}") + private String username; + + @Value("${km-password}") + private String password; + + @Value("${km-root-path}") + private String kmRootPath; + + @Value("${km-guest-user}") + private String guestUser; + + @Value("${km-guest-password}") + private String guestPassword; public OpenKMServiceImpl() { } - private static OKMWebservices connector = null; + private OKMWebservices connector; + @PostConstruct public void init() { - if (connector == null) { - url = ConfigProperties.getPropertyByName("km-base-url"); - username = ConfigProperties.getPropertyByName("km-username"); - password = ConfigProperties.getPropertyByName("km-password"); - kmRootPath = ConfigProperties.getPropertyByName("km-root-path"); - guestUser = ConfigProperties.getPropertyByName("km-guest-user"); - guestPassword = ConfigProperties.getPropertyByName("km-guest-password"); - connector = OpenKMConnector.initialize(url, username, password); - - } + logger.info("KM URL: " + url); + logger.info("KM Username: " + username); + + connector = OpenKMConnector.initialize(url, username, password); + } @Override From e1c84e8d24d8ef918afc2e419e4e00c71c0574fd Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 31 Mar 2026 16:29:47 +0530 Subject: [PATCH 50/70] fix: KM issue --- .../java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java | 2 ++ 1 file changed, 2 insertions(+) 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 acb47e6f..29c2717a 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 @@ -46,6 +46,8 @@ import com.openkm.sdk4j.exception.VirusDetectedException; import com.openkm.sdk4j.exception.WebserviceException; +import jakarta.annotation.PostConstruct; + import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.JerseyClientBuilder; From cd61d46b0137696945f1c2bbea566eb8c313bdcd Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 31 Mar 2026 16:45:34 +0530 Subject: [PATCH 51/70] fix: remove unwanted imports --- .../iemr/common/utils/km/openkm/OpenKMServiceImpl.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 29c2717a..d004243d 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,14 +48,13 @@ import jakarta.annotation.PostConstruct; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -@Service +// import org.springframework.context.annotation.Primary; +// import org.springframework.stereotype.Service; +// @Service +// @Primary public class OpenKMServiceImpl implements KMService { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); From 92d49ad2515d76f0465fd706f58300360e8572bf Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 31 Mar 2026 16:50:42 +0530 Subject: [PATCH 52/70] fix: conflicts --- .../java/com/iemr/common/utils/IEMRApplBeans.java | 12 ++++++------ .../common/utils/km/openkm/OpenKMServiceImpl.java | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java index 92d3c339..7747f6ee 100644 --- a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java +++ b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java @@ -40,12 +40,12 @@ @Configuration public class IEMRApplBeans { - @Bean - public KMService getOpenKMService() - { - KMService kmService = new OpenKMServiceImpl(); - return kmService; - } + // @Bean + // public KMService getOpenKMService() + // { + // KMService kmService = new OpenKMServiceImpl(); + // return kmService; + // } @Bean public Validator getVaidator() 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 d004243d..dd304a64 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 @@ -51,9 +51,9 @@ 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 +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +@Service // @Primary public class OpenKMServiceImpl implements KMService { From f1496fa481dedd3ebbca68fa3dbe03ec1345b91d Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 1 Apr 2026 10:19:43 +0530 Subject: [PATCH 53/70] fix: update the temp path --- .../service/kmfilemanager/KMFileManagerServiceImpl.java | 5 ++++- src/main/resources/application.properties | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java index 4297022a..b214a9ee 100644 --- a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java +++ b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java @@ -89,6 +89,9 @@ public void setSubCategoryRepository(SubCategoryRepository subCategoryRepository @Value("${allowed.file.extensions}") private String allowedFileExtensions; + @Value("${tempFilePath}") + private String tempFilePath; + @Override public String getKMFileLists(String request) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); @@ -183,7 +186,7 @@ private ArrayList addKMFile(Iterable kmFileManager .replace("}", "").replace("[", "").replace("]", "").replace("|", "").replace("\\", "") .replace(":", "").replace(";", "").replace("-", "").replace("_", "").replace("+", "") .replace("=", "").replace("\"", "").replace("'", "")); - String tempFilePath = ConfigProperties.getPropertyByName("tempFilePath"); + // String tempFilePath = ConfigProperties.getPropertyByName("tempFilePath"); newFile = new FileOutputStream(tempFilePath + "/" + kmFileManager.getFileName()); newFile.write(Base64.getDecoder().decode(kmFileManager.getFileContent())); newFile.flush(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18723465..8f8bbb4a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -28,7 +28,7 @@ BeneficiarySmsTemplate=Beneficiary UPTSU SMS future-days=7 ## Path where the files would be stored before uploading to KM server -tempFilePath=c:/temp/ +tempFilePath=/tmp ## swaasa file path lungAssessmentPath=c:/swaasa_audio/ From 72f0c28a2b6c10f134e7470c5a14fd4ed047229e Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:27:06 +0530 Subject: [PATCH 54/70] Fix the OpenKM Issue (#389) * fix: remove km in application.properties * fix: update all the properties to fetch from env * fix: update path * fix: KM issue * fix: get file from km * fix: build issue * fix: build issue * fix: remove unwanted imports * fix: build issue * fix: remove commented line * Enable KM configuration in common_example.properties Uncomment KM configuration properties for OpenKM. --- .../environment/common_example.properties | 2 + .../com/iemr/common/CommonApplication.java | 2 +- .../common/service/cti/CTIServiceImpl.java | 2 + .../service/feedback/FeedbackServiceImpl.java | 19 +++++-- .../notification/NotificationServiceImpl.java | 19 +++++-- .../service/scheme/SchemeServiceImpl.java | 24 +++++--- .../service/services/CommonServiceImpl.java | 25 +++++--- .../com/iemr/common/utils/IEMRApplBeans.java | 12 ++-- .../common/utils/JwtAuthenticationUtil.java | 4 -- .../utils/km/openkm/OpenKMServiceImpl.java | 57 ++++++++++--------- src/main/resources/application.properties | 16 +++--- 11 files changed, 113 insertions(+), 69 deletions(-) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index aca73ddb..e2b2555c 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -25,6 +25,8 @@ km-root-path=/okm:personal/users/ km-guest-user=guest km-guest-password=guest +tempFilePath=/tmp + # CTI Config cti-server-ip=10.208.122.99 cti-logger_base_url=http://10.208.122.99/logger diff --git a/src/main/java/com/iemr/common/CommonApplication.java b/src/main/java/com/iemr/common/CommonApplication.java index dff5d809..45d61800 100644 --- a/src/main/java/com/iemr/common/CommonApplication.java +++ b/src/main/java/com/iemr/common/CommonApplication.java @@ -41,7 +41,7 @@ @SpringBootApplication @EnableScheduling -@EnableAsync +@EnableAsync(proxyTargetClass = true) public class CommonApplication extends SpringBootServletInitializer { @Bean diff --git a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java index 81d8953a..3348befe 100644 --- a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java +++ b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -90,6 +91,7 @@ public class CTIServiceImpl implements CTIService { @Autowired private BeneficiaryCallRepository beneficiaryCallRepository; + @Lazy @Autowired private CTIService ctiService; diff --git a/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java b/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java index b4c7782e..f94b6e9a 100644 --- a/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java +++ b/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -104,6 +105,19 @@ public class FeedbackServiceImpl implements FeedbackService { private Logger logger = LoggerFactory.getLogger(FeedbackServiceImpl.class); // private ExecutorService executor = Executors.newCachedThreadPool(); + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + @Autowired private T_EpidemicOutbreakRepo t_EpidemicOutbreakRepo; @@ -736,10 +750,7 @@ private String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } diff --git a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java index 2d8aaeb3..a4fda6f5 100644 --- a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java @@ -38,6 +38,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -70,6 +71,19 @@ public class NotificationServiceImpl implements NotificationService private EmailService emailService; + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + @Autowired public void setEmailService(EmailService emailService) { @@ -415,10 +429,7 @@ private String getFilePath(KMFileManager kmFileManager) if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } diff --git a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java index 44e1efaa..947c2c17 100644 --- a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -59,6 +60,18 @@ public class SchemeServiceImpl implements SchemeService { private KMFileManagerService kmFileManagerService; + @Value("${km-api-base-protocol}") + private String dmsProtocol; + + @Value("${km-api-base-url}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + @Autowired public void setKmFileManagerService(KMFileManagerService kmFileManagerService) { this.kmFileManagerService = kmFileManagerService; @@ -104,16 +117,13 @@ public String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } - // return fileUIDAsURI; - String message = kmFileManager.getFileUID() ; - return message; + return fileUIDAsURI; + // String message = kmFileManager.getFileUID() ; + // return message; } @Override diff --git a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java index ff6f83e9..d8587a86 100644 --- a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java +++ b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java @@ -64,7 +64,19 @@ public class CommonServiceImpl implements CommonService { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + private static final String FILE_PATH = "filePath"; /** @@ -177,10 +189,6 @@ private String getFilePath(String fileUID) { String fileUIDAsURI = null; - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; @@ -233,12 +241,13 @@ public List getSubCategoryFilesWithURL(String request) throw SubCategoryDetails subCategory = subCategoriesList.get(index); if (subCategory.getSubCatFilePath() != null && subCategory.getSubCatFilePath().length() > 0) { String subCatFilePath = subCategory.getSubCatFilePath(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); String fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + subCategory.getSubCatFilePath(); + logger.info("file url="+fileUIDAsURI); + logger.info("file path="+subCategory.getSubCatFilePath()); + logger.info("dms Path="+dmsPath); + logger.info("subcatfilePath="+subCatFilePath); + subCategory.setSubCatFilePath(fileUIDAsURI); subCategoriesList.get(index).setFileManger(kmFileManagerRepository .getKMFileLists(subCategoryDetails.getProviderServiceMapID(), subCatFilePath)); diff --git a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java index 92d3c339..7747f6ee 100644 --- a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java +++ b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java @@ -40,12 +40,12 @@ @Configuration public class IEMRApplBeans { - @Bean - public KMService getOpenKMService() - { - KMService kmService = new OpenKMServiceImpl(); - return kmService; - } + // @Bean + // public KMService getOpenKMService() + // { + // KMService kmService = new OpenKMServiceImpl(); + // return kmService; + // } @Bean public Validator getVaidator() diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 1e9f589d..d9eb1ce2 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -13,7 +13,6 @@ import com.iemr.common.data.users.User; import com.iemr.common.repository.users.IEMRUserRepositoryCustom; -import com.iemr.common.service.users.IEMRAdminUserServiceImpl; import com.iemr.common.utils.exception.IEMRException; import io.jsonwebtoken.Claims; @@ -32,9 +31,6 @@ public class JwtAuthenticationUtil { private IEMRUserRepositoryCustom iEMRUserRepositoryCustom; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - @Autowired - private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl; - public JwtAuthenticationUtil(CookieUtil cookieUtil, JwtUtil jwtUtil) { this.cookieUtil = cookieUtil; this.jwtUtil = jwtUtil; 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 2be04cfc..4a6e68ec 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 @@ -46,44 +46,47 @@ import com.openkm.sdk4j.exception.VirusDetectedException; import com.openkm.sdk4j.exception.WebserviceException; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; +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 public class OpenKMServiceImpl implements KMService { - // private ConfigProperties configProperties; - // - // @Autowired - // public void setConfigProperties(ConfigProperties configProperties) - // { - // this.configProperties = configProperties; - // } + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private static String url; - private static String username; - private static String password; - private static String kmRootPath; - private static String guestUser; - private static String guestPassword; + @Value("${km-base-url}") + private String url; + + @Value("${km-username}") + private String username; + + @Value("${km-password}") + private String password; + + @Value("${km-root-path}") + private String kmRootPath; + + @Value("${km-guest-user}") + private String guestUser; + + @Value("${km-guest-password}") + private String guestPassword; public OpenKMServiceImpl() { } - private static OKMWebservices connector = null; + private OKMWebservices connector; + @PostConstruct public void init() { - if (connector == null) { - url = ConfigProperties.getPropertyByName("km-base-url"); - username = ConfigProperties.getPropertyByName("km-username"); - password = ConfigProperties.getPropertyByName("km-password"); - kmRootPath = ConfigProperties.getPropertyByName("km-root-path"); - guestUser = ConfigProperties.getPropertyByName("km-guest-user"); - guestPassword = ConfigProperties.getPropertyByName("km-guest-password"); - connector = OpenKMConnector.initialize(url, username, password); + logger.info("KM URL=",url); + connector = OpenKMConnector.initialize(url, username, password); - } - } + } @Override public String getDocumentRoot() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a3de6772..c44efdf0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -181,14 +181,14 @@ jwt.refresh.expiration=604800000 ## KM Configuration -km-base-protocol=http -km-username=okmAdmin -km-password=admin -km-base-url=http://localhost:8084/OpenKM -km-base-path=localhost:8084/OpenKM -km-root-path=/okm:personal/users/ -km-guest-user=guest -km-guest-password=guest +# km-base-protocol=http +# km-username=okmAdmin +# km-password=admin +# km-base-url=http://localhost:8084/OpenKM +# km-base-path=localhost:8084/OpenKM +# km-root-path=/okm:personal/users/ +# km-guest-user=guest +# km-guest-password=guest # CTI Config cti-server-ip=10.208.122.99 From 79b050da315571512b9cfa3b7e8b59e3e37315b3 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:14:18 +0530 Subject: [PATCH 55/70] Fix ConfigProperties to resolve env variable placeholders via Spring Environment (#390) Co-authored-by: Claude Opus 4.6 (1M context) --- .../common/utils/config/ConfigProperties.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java index 59b69b82..43a49364 100644 --- a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java +++ b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java @@ -144,11 +144,21 @@ public static String getPropertyByName(String propertyName) String result = null; try { - if (properties == null) + if (environment != null) { - initalizeProperties(); + result = environment.getProperty(propertyName); + } + if (result == null) + { + if (properties == null) + { + initalizeProperties(); + } + result = properties.getProperty(propertyName).trim(); + } else + { + result = result.trim(); } - result = properties.getProperty(propertyName).trim(); } catch (Exception e) { logger.error(propertyName + " retrival failed.", e); From 31ff8211940c2c5a3604b4b60c20dcae8df5134c Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 8 Apr 2026 22:22:48 +0530 Subject: [PATCH 56/70] fix: update sms issue --- src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index efe0d16a..af443ffa 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -391,8 +391,8 @@ public SMSNotification prepareVideoCallSMS(SMSRequest request, VideoCallParamete String variable = smsParametersMap.getSmsParameterName(); String methodName = smsParametersMap.getSmsParameter().getDataName(); String variableValue = ""; - variableValue = getVideoCallData(methodName, vcParams); - smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); + // variableValue = getVideoCallData(methodName, vcParams); + // smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); if ("VideoCall".equalsIgnoreCase(smsParametersMap.getSmsParameter().getSmsParameterType())) { variableValue = getVideoCallData(methodName, vcParams); From ac388d221fd3b806453e9a04d5fc7aca3960ab3d Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 8 Apr 2026 22:44:58 +0530 Subject: [PATCH 57/70] fix: build issue --- src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java index 7a6a2e4b..5536a721 100644 --- a/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java +++ b/src/main/java/com/iemr/common/service/cti/CTIServiceImpl.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; From 00aa6daa5254bf30c3908df14505d2d2195dbe45 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 8 Apr 2026 23:24:14 +0530 Subject: [PATCH 58/70] fix: update condition --- .../common/service/sms/SMSServiceImpl.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index af443ffa..758bf59b 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -436,10 +436,15 @@ public String getVideoCallData(String methodName, VideoCallParameters videoCall) variableValue = videoCall.getCallerPhoneNumber() !=null ? videoCall.getCallerPhoneNumber().toString() : ""; break; default: - Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); - method.setAccessible(true); - Object result = method.invoke(videoCall); - variableValue = result != null ? result.toString() : ""; + try { + Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); + method.setAccessible(true); + Object result = method.invoke(videoCall); + variableValue = result != null ? result.toString() : ""; + } catch (NoSuchMethodException e) { + logger.warn("No getter found for methodName: " + methodName + " on VideoCallParameters"); + variableValue = ""; + } break; } return variableValue.trim(); @@ -875,9 +880,16 @@ private String getBeneficiaryData(String className, String methodName, SMSReques variableValue = imrName; break; default: - Class clazz = Class.forName(className); - Method method = clazz.getDeclaredMethod("get" + methodName, null); - variableValue = method.invoke(beneficiary, null).toString(); + if ("com.iemr.common.data.videocall.VideoCallParameters".equals(className)) { + VideoCallParameters vcParams = getVideoCallParameters(request.getSmsAdvice()); + if (vcParams != null) { + variableValue = getVideoCallData(methodName, vcParams); + } + } else { + Class clazz = Class.forName(className); + Method method = clazz.getDeclaredMethod("get" + methodName, null); + variableValue = method.invoke(beneficiary, null).toString(); + } break; } From c49d792a37ade00cee80a06528ebdc2bf6ff60e6 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Thu, 9 Apr 2026 09:24:24 +0530 Subject: [PATCH 59/70] fix: edit ben issue --- .../service/beneficiary/RegisterBenificiaryServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java index 1dd0b533..ac084e15 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java @@ -120,7 +120,7 @@ private void updateBeneficiaryID(String beneficiaryID, Long beneficiaryRegID) { } } - @Async + // @Async @Override public Integer updateBenificiary(BeneficiaryModel benificiaryDetails, String auth) throws IEMRException { Integer updatedRows = 0; From 9bbfdfc120e654405a7e986d280ff2d705bb0ee7 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Thu, 9 Apr 2026 10:40:27 +0530 Subject: [PATCH 60/70] fix: phone number issue for sms --- .../java/com/iemr/common/service/sms/SMSServiceImpl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index 758bf59b..c49eca10 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -683,7 +683,7 @@ private SMSNotification prepareSMS( sms.setReceivingUserID(request.getUserID()); String smsToSend = ""; BeneficiaryModel beneficiary = null; - if (request.getBeneficiaryRegID() != null) { + if (request.getBeneficiaryRegID() != null && !request.getBeneficiaryRegID().toString().isEmpty()) { List beneficiaries = searchBeneficiary.userExitsCheckWithId(request.getBeneficiaryRegID(), authToken, request.getIs1097()); if (beneficiaries.size() == 1) @@ -849,6 +849,12 @@ private String getUserData(String className, String methodName, SMSRequest reque private String getBeneficiaryData(String className, String methodName, SMSRequest request, BeneficiaryModel beneficiary) throws Exception { String variableValue = ""; + if (beneficiary == null) { + if ("phoneno".equalsIgnoreCase(methodName)) { + return request.getBenPhoneNo() != null ? request.getBenPhoneNo() : ""; + } + return ""; + } switch (methodName.toLowerCase()) { case "name": String fname = beneficiary.getFirstName() != null ? beneficiary.getFirstName() + " " : ""; From 61cc94e466e9727f000302c04baee2d540618f68 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Thu, 9 Apr 2026 15:05:45 +0530 Subject: [PATCH 61/70] fix: update the url with jwt token --- src/main/environment/common_ci.properties | 9 ++ src/main/environment/common_docker.properties | 9 ++ .../environment/common_example.properties | 8 ++ .../videocall/VideoCallController.java | 36 ++++- .../service/videocall/VideoCallService.java | 14 +- .../videocall/VideoCallServiceImpl.java | 49 ++++++- .../com/iemr/common/utils/JitsiJwtUtil.java | 108 +++++++++++++++ .../utils/JwtUserIdValidationFilter.java | 6 +- .../videocall/VideoCallControllerTest.java | 37 +++++ .../videocall/VideoCallServiceImplTest.java | 69 ++++++++++ .../iemr/common/utils/JitsiJwtUtilTest.java | 126 ++++++++++++++++++ 11 files changed, 466 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/iemr/common/utils/JitsiJwtUtil.java create mode 100644 src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 92c0be50..4a21c0bf 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -194,6 +194,15 @@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ videocall.url=@env.VIDEO_CALL_URL@ video.recording.path=@env.VIDEO_RECORDING_PATH@ +# Jitsi JWT (prosody token-auth) +jitsi.app.id=@env.JITSI_APP_ID@ +jitsi.app.secret=@env.JITSI_APP_SECRET@ +jitsi.domain=@env.JITSI_DOMAIN@ +jitsi.sub=@env.JITSI_SUB@ +jitsi.token.ttl.seconds=@env.JITSI_TOKEN_TTL_SECONDS@ +jitsi.room.prefix=@env.JITSI_ROOM_PREFIX@ +jitsi.default.user.email=@env.JITSI_DEFAULT_USER_EMAIL@ + platform.feedback.ratelimit.enabled=@env.PLATFORM_FEEDBACK_RATELIMIT_ENABLED@ platform.feedback.ratelimit.pepper=@env.PLATFORM_FEEDBACK_RATELIMIT_PEPPER@ platform.feedback.ratelimit.trust-forwarded-for=@env.PLATFORM_FEEDBACK_RATELIMIT_TRUST_FORWARDED_FOR@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 5d674bfc..64266613 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -196,6 +196,15 @@ videocall.url=${VIDEO_CALL_URL} jibri.output.path={JIBRI_OUTPUT_PATH} video.recording.path={VIDEO_RECORDING_PATH} +# Jitsi JWT (prosody token-auth) +jitsi.app.id=${JITSI_APP_ID} +jitsi.app.secret=${JITSI_APP_SECRET} +jitsi.domain=${JITSI_DOMAIN} +jitsi.sub=${JITSI_SUB} +jitsi.token.ttl.seconds=${JITSI_TOKEN_TTL_SECONDS} +jitsi.room.prefix=${JITSI_ROOM_PREFIX} +jitsi.default.user.email=${JITSI_DEFAULT_USER_EMAIL} + # Platform Feedback module platform.feedback.ratelimit.enabled=${PLATFORM_FEEDBACK_RATELIMIT_ENABLED} platform.feedback.ratelimit.pepper=${PLATFORM_FEEDBACK_RATELIMIT_PEPPER} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 2f13e4f1..288c3ec5 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -228,3 +228,11 @@ platform.feedback.ratelimit.backoff-minutes=15 ### generate Beneficiary IDs URL generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs + +JITSI_APP_ID=piramal_vc +JITSI_APP_SECRET=5b9883418be6f228ffe3ceaa74dd3d3b91737733a4a85c5e82fc584ad449850b +JITSI_DOMAIN=vc.piramalswasthya.org +JITSI_SUB=meet.jitsi +JITSI_TOKEN_TTL_SECONDS=3600 +JITSI_ROOM_PREFIX=piramal-meeting- +JITSI_DEFAULT_USER_EMAIL=admin@piramalswasthya.org diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index fdbd9575..671133c5 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -22,6 +22,7 @@ package com.iemr.common.controller.videocall; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -32,8 +33,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.iemr.common.model.videocall.UpdateCallRequest; @@ -113,6 +116,35 @@ public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest re return ResponseEntity.ok(response.toString()); } - - +/** + * Public redirect endpoint hit when a beneficiary clicks the short SMS link. + * + * Flow: + * 1. Jitsi host nginx receives "https://vc.piramalswasthya.org/?m=<slug>" + * and proxies/redirects it to this endpoint. + * 2. This endpoint looks up the slug, mints a fresh Jitsi JWT bound to the + * room and the agent, and 302-redirects the browser to the full Jitsi URL + * "https://vc.piramalswasthya.org/<room>?jwt=<token>". + * 3. The Jitsi server enforces the JWT (prosody token-auth) and admits the user. + * + * Intentionally NOT guarded by Authorization header - the SMS recipient is on + * a phone browser and has no app session. Access control is the JWT itself + * plus the slug being unguessable and the meeting row existing. + */ +@GetMapping(value = "/resolve") +public ResponseEntity resolveMeetingLink(@RequestParam("m") String slug) { + try { + String redirectUrl = videoCallService.resolveMeetingLink(slug); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } catch (IllegalArgumentException e) { + logger.warn("resolveMeetingLink rejected: {}", e.getMessage()); + return ResponseEntity.badRequest().build(); + } catch (Exception e) { + logger.error("resolveMeetingLink failed for slug={}: {}", slug, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } +} + } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java index dd457688..096c000b 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java @@ -26,10 +26,22 @@ import com.iemr.common.model.videocall.VideoCallRequest; public interface VideoCallService { - + public String generateMeetingLink() throws Exception; public String sendMeetingLink(VideoCallRequest request) throws Exception; public String updateCallStatus(UpdateCallRequest request) throws Exception; + + /** + * Resolve the short slug carried in the SMS link (the value after "?m=") + * into the full Jitsi URL with a freshly minted JWT appended. + * Called by the public redirect endpoint that the Jitsi host's nginx + * proxies "/?m=<slug>" requests to. + * + * @param slug the random slug originally generated by {@link #generateMeetingLink()} + * @return absolute URL of the form + * https://<jitsi.domain>/<jitsi.room.prefix><slug>?jwt=<token> + */ + public String resolveMeetingLink(String slug) throws Exception; } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 8b61d525..2669ed92 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -40,6 +40,7 @@ import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.config.ConfigProperties; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -51,10 +52,13 @@ public class VideoCallServiceImpl implements VideoCallService { @Autowired private VideoCallParameterRepository videoCallRepository; - + @Autowired private VideoCallMapper videoCallMapper; + @Autowired + private JitsiJwtUtil jitsiJwtUtil; + private String meetingLink; private boolean isLinkSent = false; @@ -62,6 +66,17 @@ public class VideoCallServiceImpl implements VideoCallService { @Value("${videocall.url}") private String jitsiLink; + // Fallback chains let either dot-form or JITSI_*-form work in any property + // source (.properties files do NOT get Spring relaxed binding for @Value). + @Value("${jitsi.domain:${JITSI_DOMAIN:vc.piramalswasthya.org}}") + private String jitsiDomain; + + @Value("${jitsi.room.prefix:${JITSI_ROOM_PREFIX:piramal-meeting-}}") + private String roomPrefix; + + @Value("${jitsi.default.user.email:${JITSI_DEFAULT_USER_EMAIL:admin@piramalswasthya.org}}") + private String defaultUserEmail; + public VideoCallServiceImpl() { // Default constructor this.meetingLink = null; @@ -130,6 +145,38 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { .toJson(videoCallMapper.videoCallToResponse(videoCall)); } +@Override +public String resolveMeetingLink(String slug) throws Exception { + if (slug == null || slug.isEmpty()) { + throw new IllegalArgumentException("Meeting slug is required"); + } + + // The persisted meetingLink is the short URL produced by generateMeetingLink(), + // i.e. "m=". Reconstruct it to look up the row. + String shortLink = jitsiLink + "m=" + slug; + VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); + + if (params == null) { + throw new Exception("No meeting found for slug: " + slug); + } + + // Note: we deliberately do NOT block on linkUsed=true here, because real + // calls drop and the agent/beneficiary often have to rejoin. The linkUsed + // flag is for reporting in updateCallStatus, not access control. Access + // control comes from the JWT exp + the room claim. + + String roomName = roomPrefix + slug; + String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() + ? params.getAgentName() + : "Guest"; + + String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail); + + String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; + logger.info("Resolved slug {} -> room {}", slug, roomName); + return redirectUrl; +} + } diff --git a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java new file mode 100644 index 00000000..4c6be54e --- /dev/null +++ b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java @@ -0,0 +1,108 @@ +/* +* 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.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +/** + * Mints HS256 JWTs that are accepted by the Jitsi/prosody token-auth module + * running on the video-conferencing host. This is intentionally separate from + * {@link JwtUtil} (which mints application session tokens) because the secret, + * claim set, and expiration policy are completely different. + * + * Claims produced (matches what devops configured on prosody): + * aud -> jitsi.app.id (e.g. "piramal_vc") + * iss -> jitsi.app.id (e.g. "piramal_vc") + * sub -> jitsi.sub (must always be "meet.jitsi") + * room -> the room name to admit the bearer into + * exp -> now + jitsi.token.ttl.seconds + * context.user.{name,email} -> displayed in the Jitsi UI + */ +@Component +public class JitsiJwtUtil { + + // Fallback chains let either dot-form (jitsi.app.id=...) or upper-form + // (JITSI_APP_ID=...) work in any property source, including .properties + // files which Spring does NOT relaxed-bind for @Value. + @Value("${jitsi.app.id:${JITSI_APP_ID:}}") + private String appId; + + @Value("${jitsi.app.secret:${JITSI_APP_SECRET:}}") + private String appSecret; + + @Value("${jitsi.sub:${JITSI_SUB:meet.jitsi}}") + private String sub; + + @Value("${jitsi.token.ttl.seconds:${JITSI_TOKEN_TTL_SECONDS:3600}}") + private long ttlSeconds; + + private SecretKey getSigningKey() { + if (appSecret == null || appSecret.isEmpty()) { + throw new IllegalStateException("jitsi.app.secret is not configured"); + } + return Keys.hmacShaKeyFor(appSecret.getBytes()); + } + + /** + * Build a Jitsi room JWT. + * + * @param room the exact room name the bearer will join (must match the URL path) + * @param userName display name shown in the Jitsi UI + * @param userEmail email shown in the Jitsi UI (used for gravatar etc.) + * @return signed compact JWT string + */ + public String generateRoomToken(String room, String userName, String userEmail) { + if (room == null || room.isEmpty()) { + throw new IllegalArgumentException("room is required to mint a Jitsi token"); + } + + long nowMs = System.currentTimeMillis(); + Date expiry = new Date(nowMs + (ttlSeconds * 1000L)); + + Map user = new HashMap<>(); + user.put("name", userName != null ? userName : "Guest"); + user.put("email", userEmail != null ? userEmail : ""); + + Map context = new HashMap<>(); + context.put("user", user); + + return Jwts.builder() + .audience().add(appId).and() + .issuer(appId) + .subject(sub) + .claim("room", room) + .claim("context", context) + .expiration(expiry) + .signWith(getSigningKey()) + .compact(); + } +} diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 81d79221..a0358da0 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -251,7 +251,11 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/userLogout") || path.startsWith(contextPath + "/user/validateSecurityQuestionAndAnswer") || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") - || path.startsWith(contextPath + "/user/refreshToken"); + || path.startsWith(contextPath + "/user/refreshToken") + // Public Jitsi short-link redirect: hit by SMS recipients on phone + // browsers that have no app session. Access control is the JWT minted + // inside the redirect handler + the unguessable slug. + || path.equals(contextPath + "/video-consultation/resolve"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java index b3b82380..705beffa 100644 --- a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java +++ b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java @@ -36,7 +36,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -195,4 +197,39 @@ void shouldReturnOkWithErrorInBody_whenUpdateCallStatusServiceFails() throws Exc verify(videoCallService, times(1)).updateCallStatus(any(UpdateCallRequest.class)); } + + // Tests for resolveMeetingLink() - public redirect endpoint hit by SMS recipients + @Test + void shouldReturn302WithJitsiUrl_whenResolveMeetingLinkSucceeds() throws Exception { + String fullJitsiUrl = "https://vc.piramalswasthya.org/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN"; + when(videoCallService.resolveMeetingLink(eq("Ab3xQ9pK"))).thenReturn(fullJitsiUrl); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "Ab3xQ9pK")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", fullJitsiUrl)); + + verify(videoCallService, times(1)).resolveMeetingLink("Ab3xQ9pK"); + } + + @Test + void shouldReturn400_whenResolveMeetingLinkSlugIsInvalid() throws Exception { + when(videoCallService.resolveMeetingLink(eq(""))) + .thenThrow(new IllegalArgumentException("Meeting slug is required")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "")) + .andExpect(status().isBadRequest()); + + verify(videoCallService, times(1)).resolveMeetingLink(""); + } + + @Test + void shouldReturn404_whenResolveMeetingLinkSlugUnknown() throws Exception { + when(videoCallService.resolveMeetingLink(eq("missing"))) + .thenThrow(new Exception("No meeting found for slug: missing")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "missing")) + .andExpect(status().isNotFound()); + + verify(videoCallService, times(1)).resolveMeetingLink("missing"); + } } \ No newline at end of file diff --git a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java index baed9029..2723fda0 100644 --- a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java +++ b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java @@ -26,6 +26,7 @@ import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.config.ConfigProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,10 +60,15 @@ public class VideoCallServiceImplTest { UpdateCallRequest updateCallRequest; @Mock VideoCallParameters videoCallParameters; + @Mock + JitsiJwtUtil jitsiJwtUtil; @BeforeEach public void setup() throws Exception { ReflectionTestUtils.setField(service, "jitsiLink", "https://meet.jit.si/"); + ReflectionTestUtils.setField(service, "jitsiDomain", "meet.jit.si"); + ReflectionTestUtils.setField(service, "roomPrefix", "piramal-meeting-"); + ReflectionTestUtils.setField(service, "defaultUserEmail", "admin@piramalswasthya.org"); } @Test @@ -175,6 +181,69 @@ public void testSaveRecordingFile_noMatchingFile() throws Exception { } } + @Test + public void testResolveMeetingLink_success() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn("Dr. Asha"); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Dr. Asha"), + eq("admin@piramalswasthya.org"))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertEquals( + "https://meet.jit.si/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN", + result); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Dr. Asha", "admin@piramalswasthya.org"); + } + + @Test + public void testResolveMeetingLink_emptySlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink("")); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_nullSlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink(null)); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_notFound() { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=missing")) + .thenReturn(null); + + Exception ex = assertThrows( + Exception.class, + () -> service.resolveMeetingLink("missing")); + assertTrue(ex.getMessage().contains("No meeting found")); + } + + @Test + public void testResolveMeetingLink_fallbackUserNameWhenAgentMissing() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn(null); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Guest"), + eq("admin@piramalswasthya.org"))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertTrue(result.endsWith("?jwt=FAKE.JWT.TOKEN")); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Guest", "admin@piramalswasthya.org"); + } + @Test public void testSaveRecordingFile_ioException() throws Exception { try (MockedStatic configMock = mockStatic(ConfigProperties.class); diff --git a/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java new file mode 100644 index 00000000..c300e27b --- /dev/null +++ b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java @@ -0,0 +1,126 @@ +/* +* 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +class JitsiJwtUtilTest { + + // Same secret format as the one devops gave us (HS256, length must be >=32 bytes for Keys.hmacShaKeyFor) + private static final String APP_ID = "piramal_vc"; + private static final String APP_SECRET = "5b9883418be6f228ffe3ceaa74dd3d3b91737733a4a85c5e82fc584ad449850b"; + private static final String SUB = "meet.jitsi"; + + private JitsiJwtUtil util; + + @BeforeEach + void setUp() { + util = new JitsiJwtUtil(); + ReflectionTestUtils.setField(util, "appId", APP_ID); + ReflectionTestUtils.setField(util, "appSecret", APP_SECRET); + ReflectionTestUtils.setField(util, "sub", SUB); + ReflectionTestUtils.setField(util, "ttlSeconds", 3600L); + } + + @Test + void generateRoomToken_producesAllRequiredClaims() { + String token = util.generateRoomToken("piramal-meeting-Ab3xQ9pK", "Dr. Asha", "asha@piramalswasthya.org"); + + assertNotNull(token); + assertTrue(token.split("\\.").length == 3, "JWT should have 3 dot-separated parts"); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + assertEquals(APP_ID, claims.getIssuer()); + assertTrue(claims.getAudience().contains(APP_ID)); + assertEquals(SUB, claims.getSubject()); + assertEquals("piramal-meeting-Ab3xQ9pK", claims.get("room", String.class)); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + assertNotNull(context); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertNotNull(user); + assertEquals("Dr. Asha", user.get("name")); + assertEquals("asha@piramalswasthya.org", user.get("email")); + + Date exp = claims.getExpiration(); + assertNotNull(exp); + assertTrue(exp.after(new Date()), "exp should be in the future"); + } + + @Test + void generateRoomToken_fallsBackToGuestWhenUserNameNull() { + String token = util.generateRoomToken("piramal-meeting-xyz", null, null); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertEquals("Guest", user.get("name")); + assertEquals("", user.get("email")); + } + + @Test + void generateRoomToken_rejectsEmptyRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken("", "Dr. Asha", "asha@piramalswasthya.org")); + } + + @Test + void generateRoomToken_rejectsNullRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken(null, "Dr. Asha", "asha@piramalswasthya.org")); + } + + @Test + void generateRoomToken_failsWhenAppSecretMissing() { + ReflectionTestUtils.setField(util, "appSecret", ""); + assertThrows(IllegalStateException.class, + () -> util.generateRoomToken("piramal-meeting-xyz", "Dr. Asha", "asha@piramalswasthya.org")); + } +} From a48b4afe6a9f35a7db3e33a31263e9c7e8888e9b Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Fri, 10 Apr 2026 14:46:20 +0530 Subject: [PATCH 62/70] fix: jitsi authorization issue --- src/main/java/com/iemr/common/config/InterceptorConfig.java | 3 ++- .../java/com/iemr/common/utils/JwtUserIdValidationFilter.java | 2 +- .../com/iemr/common/utils/http/HTTPRequestInterceptor.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/config/InterceptorConfig.java b/src/main/java/com/iemr/common/config/InterceptorConfig.java index a321eb08..b39a7358 100644 --- a/src/main/java/com/iemr/common/config/InterceptorConfig.java +++ b/src/main/java/com/iemr/common/config/InterceptorConfig.java @@ -36,7 +36,8 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(requestInterceptor); + registry.addInterceptor(requestInterceptor) + .excludePathPatterns("/video-consultation/resolve"); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index a0358da0..94c4b8ca 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -255,7 +255,7 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { // Public Jitsi short-link redirect: hit by SMS recipients on phone // browsers that have no app session. Access control is the JWT minted // inside the redirect handler + the unguessable slug. - || path.equals(contextPath + "/video-consultation/resolve"); + || path.endsWith("/video-consultation/resolve"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index b4aaad60..757e59d9 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -125,6 +125,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons case "validateSecurityQuestionAndAnswer": case "logOutUserFromConcurrentSession": case "refreshToken": + case "resolve": break; case "error": status = false; From 389c20de7f9de04e26f14bd66e877d9cb25a5e29 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Fri, 10 Apr 2026 15:55:49 +0530 Subject: [PATCH 63/70] fix: skip auth --- .../iemr/common/config/InterceptorConfig.java | 2 +- .../utils/JwtUserIdValidationFilter.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/config/InterceptorConfig.java b/src/main/java/com/iemr/common/config/InterceptorConfig.java index b39a7358..8a45482a 100644 --- a/src/main/java/com/iemr/common/config/InterceptorConfig.java +++ b/src/main/java/com/iemr/common/config/InterceptorConfig.java @@ -37,7 +37,7 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(requestInterceptor) - .excludePathPatterns("/video-consultation/resolve"); + .excludePathPatterns("/video-consultation/resolve", "**/video-consultation/resolve"); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 94c4b8ca..ec841a4d 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -120,6 +120,15 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("JwtUserIdValidationFilter invoked for path: " + path); + // Public video-consultation resolve endpoint: hit by SMS recipients on + // phone browsers that have no app session. Skip ALL auth — the JWT minted + // inside the handler + the unguessable slug provide access control. + if (isVideoConsultationResolvePath(path, contextPath)) { + logger.info("Video-consultation resolve path detected - skipping authentication: {}", path); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + // NEW: if this is a platform-feedback endpoint, treat it as public (skip auth) // and also ensure we don't clear any user cookies for these requests. if (isPlatformFeedbackPath(path, contextPath)) { @@ -206,6 +215,17 @@ private boolean isPlatformFeedbackPath(String path, String contextPath) { return normalized.startsWith(base + "/platform-feedback"); } + /** + * Identifies the public video-consultation resolve endpoint. + * Uses multiple matching strategies to be resilient against + * context-path mismatches between reverse-proxy and Wildfly. + */ + private boolean isVideoConsultationResolvePath(String path, String contextPath) { + if (path == null) return false; + String normalized = path.toLowerCase(); + return normalized.endsWith("/video-consultation/resolve") + || normalized.contains("/video-consultation/resolve"); + } private boolean isOriginAllowed(String origin) { if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) { From 1fc65093e63cb6c6f42446f0e28d13995e0e7b32 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Fri, 10 Apr 2026 17:38:15 +0530 Subject: [PATCH 64/70] fix: hash key updation --- src/main/java/com/iemr/common/utils/JitsiJwtUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java index 4c6be54e..a565c599 100644 --- a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java @@ -96,13 +96,13 @@ public String generateRoomToken(String room, String userName, String userEmail) context.put("user", user); return Jwts.builder() - .audience().add(appId).and() + .claim("aud", appId) .issuer(appId) .subject(sub) .claim("room", room) .claim("context", context) .expiration(expiry) - .signWith(getSigningKey()) + .signWith(getSigningKey(), Jwts.SIG.HS256) .compact(); } } From 640841b52e9d38d3f235596fd635b422cc38ac44 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Fri, 10 Apr 2026 18:05:15 +0530 Subject: [PATCH 65/70] fix: jwt type in header for authorization --- src/main/java/com/iemr/common/utils/JitsiJwtUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java index a565c599..75591633 100644 --- a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java @@ -96,6 +96,7 @@ public String generateRoomToken(String room, String userName, String userEmail) context.put("user", user); return Jwts.builder() + .header().add("typ", "JWT").and() .claim("aud", appId) .issuer(appId) .subject(sub) From 677c051488a842eec8303a9e892c01ac3b486dc0 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Fri, 17 Apr 2026 16:37:34 +0530 Subject: [PATCH 66/70] fix: update file path --- src/main/environment/common_docker.properties | 1 - .../environment/common_example.properties | 2 +- .../data/videocall/VideoCallParameters.java | 3 ++ .../model/videocall/UpdateCallRequest.java | 7 ++-- .../model/videocall/UpdateCallResponse.java | 1 + .../videocall/VideoCallServiceImpl.java | 39 +++++++++++++++---- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 64266613..a381858a 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -193,7 +193,6 @@ firebase.credential-file=${FIREBASE_CREDENTIAL} videocall.url=${VIDEO_CALL_URL} -jibri.output.path={JIBRI_OUTPUT_PATH} video.recording.path={VIDEO_RECORDING_PATH} # Jitsi JWT (prosody token-auth) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 288c3ec5..9a4ca2e0 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -202,7 +202,7 @@ logging.file.name=logs/common-api.log # Jitsi configuration videocall.url=https://vc.piramalswasthya.org/? -video.recording.path=/srv/recordings +video.recording.path=/opt/recordings captcha.secret-key= captcha.verify-url= https://challenges.cloudflare.com/turnstile/v0/siteverify diff --git a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java index 48a7b8ae..a852be81 100644 --- a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java +++ b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java @@ -79,6 +79,9 @@ public class VideoCallParameters { @Column(name = "IsLinkUsed") private boolean linkUsed; + @Column(name = "RecordingFileName") + private String recordingFileName; + @Column(name = "Deleted", insertable = false, updatable = true) private Boolean deleted; diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java index 681d4b63..39cc4e13 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java @@ -26,9 +26,10 @@ @Data public class UpdateCallRequest { - + private String meetingLink; - private String callStatus; - private String callDuration; + private String callStatus; + private String callDuration; private String modifiedBy; + private Boolean isLinkUsed; } diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java index 44c01215..8843f887 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java @@ -35,6 +35,7 @@ public class UpdateCallResponse { private String callDuration; private String modifiedBy; private boolean isLinkUsed; + private String recordingFileName; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Timestamp lastModified; diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 2669ed92..f6a30554 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -66,15 +66,13 @@ public class VideoCallServiceImpl implements VideoCallService { @Value("${videocall.url}") private String jitsiLink; - // Fallback chains let either dot-form or JITSI_*-form work in any property - // source (.properties files do NOT get Spring relaxed binding for @Value). - @Value("${jitsi.domain:${JITSI_DOMAIN:vc.piramalswasthya.org}}") + @Value("${jitsi.domain}") private String jitsiDomain; - @Value("${jitsi.room.prefix:${JITSI_ROOM_PREFIX:piramal-meeting-}}") + @Value("${jitsi.room.prefix}") private String roomPrefix; - @Value("${jitsi.default.user.email:${JITSI_DEFAULT_USER_EMAIL:admin@piramalswasthya.org}}") + @Value("${jitsi.default.user.email}") private String defaultUserEmail; public VideoCallServiceImpl() { @@ -134,9 +132,13 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { ); if (updateCount > 0) { - videoCall.setLinkUsed(true); - videoCallRepository.save(videoCall); - + // End-consultation: UI sends isLinkUsed=true; fall back to true for + // backwards compatibility with older callers that didn't send the flag. + boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); + videoCall.setLinkUsed(linkUsed); + videoCall.setRecordingFileName(buildRecordingFileName(requestEntity.getMeetingLink())); + videoCallRepository.save(videoCall); + } else { throw new Exception("Failed to update the call status"); } @@ -145,6 +147,27 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { .toJson(videoCallMapper.videoCallToResponse(videoCall)); } +/** + * Jibri records each Jitsi room into a directory named after the room, with + * the MP4 file sharing the same name — e.g. piramal-meeting-Ab3xQ9pK/piramal-meeting-Ab3xQ9pK.mp4. + * The short SMS link is "m=", so derive the room from the slug. + */ +private String buildRecordingFileName(String meetingLink) { + if (meetingLink == null) { + return null; + } + int idx = meetingLink.lastIndexOf("m="); + if (idx < 0) { + return null; + } + String slug = meetingLink.substring(idx + 2); + if (slug.isEmpty()) { + return null; + } + String roomName = roomPrefix + slug; + return roomName + "/" + roomName + ".mp4"; +} + @Override public String resolveMeetingLink(String slug) throws Exception { if (slug == null || slug.isEmpty()) { From 2a86dbd3c7704fda157cc32cbf5c39a002bc8967 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 20 Apr 2026 10:43:02 +0530 Subject: [PATCH 67/70] fix: vc recording path updation --- .../videocall/VideoCallController.java | 18 +-- .../VideoCallParameterRepository.java | 27 ++-- .../videocall/VideoCallServiceImpl.java | 115 ++++++++++++++---- 3 files changed, 122 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index 671133c5..94de8357 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -35,19 +35,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.service.videocall.VideoCallService; import com.iemr.common.utils.response.OutputResponse; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.web.bind.annotation.RequestBody; +import jakarta.servlet.http.HttpServletRequest; @RestController @RequestMapping(value = "/video-consultation") @@ -142,9 +142,13 @@ public ResponseEntity resolveMeetingLink(@RequestParam("m") String slug) { logger.warn("resolveMeetingLink rejected: {}", e.getMessage()); return ResponseEntity.badRequest().build(); } catch (Exception e) { - logger.error("resolveMeetingLink failed for slug={}: {}", slug, e.getMessage(), e); - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + logger.error("resolveMeetingLink failed for slug={}: {}", slug, e.getMessage(), e); + + // Distinguish "link expired" from "not found" with a 410 Gone + if (e.getMessage() != null && e.getMessage().contains("already been used")) { + return ResponseEntity.status(HttpStatus.GONE).build(); // 410 } + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); +} } - } diff --git a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java index 99798da2..e9f81f44 100644 --- a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java +++ b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java @@ -33,7 +33,6 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.annotation.Transactional; - @Repository public interface VideoCallParameterRepository extends CrudRepository { @@ -41,14 +40,28 @@ public interface VideoCallParameterRepository extends CrudRepository 0) { +// // End-consultation: UI sends isLinkUsed=true; fall back to true for +// // backwards compatibility with older callers that didn't send the flag. +// boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); +// videoCall.setLinkUsed(linkUsed); +// videoCall.setRecordingFileName(buildRecordingFileName(requestEntity.getMeetingLink())); +// videoCallRepository.save(videoCall); + +// } else { +// throw new Exception("Failed to update the call status"); +// } + +// return OutputMapper.gsonWithoutExposeRestriction() +// .toJson(videoCallMapper.videoCallToResponse(videoCall)); +// } + + @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { - VideoCallParameters videoCall = null; - VideoCallParameters requestEntity = videoCallMapper.updateRequestToVideoCall(callRequest); + String meetingLink = callRequest.getMeetingLink(); + + VideoCallParameters videoCall = videoCallRepository.findByMeetingLink(meetingLink); + if (videoCall == null) { + throw new Exception("No meeting found for link: " + meetingLink); + } - videoCall = videoCallRepository.findByMeetingLink(requestEntity.getMeetingLink()); + boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); + String recordingFileName = buildRecordingFileName(meetingLink); - int updateCount = videoCallRepository.updateCallStatusByMeetingLink( - requestEntity.getMeetingLink(), - requestEntity.getCallStatus(), - requestEntity.getCallDuration(), - requestEntity.getModifiedBy() + int updateCount = videoCallRepository.updateCallStatusAndRecording( + meetingLink, + callRequest.getCallStatus(), + callRequest.getCallDuration(), + callRequest.getModifiedBy(), + linkUsed, + recordingFileName ); - if (updateCount > 0) { - // End-consultation: UI sends isLinkUsed=true; fall back to true for - // backwards compatibility with older callers that didn't send the flag. - boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); - videoCall.setLinkUsed(linkUsed); - videoCall.setRecordingFileName(buildRecordingFileName(requestEntity.getMeetingLink())); - videoCallRepository.save(videoCall); - - } else { + if (updateCount == 0) { throw new Exception("Failed to update the call status"); } + // Refresh the entity to reflect DB state before serialising + videoCall = videoCallRepository.findByMeetingLink(meetingLink); + + logger.info("Call ended — link={}, isLinkUsed={}, recording={}", + meetingLink, linkUsed, recordingFileName); + return OutputMapper.gsonWithoutExposeRestriction() .toJson(videoCallMapper.videoCallToResponse(videoCall)); } @@ -168,14 +205,44 @@ private String buildRecordingFileName(String meetingLink) { return roomName + "/" + roomName + ".mp4"; } +// @Override +// public String resolveMeetingLink(String slug) throws Exception { +// if (slug == null || slug.isEmpty()) { +// throw new IllegalArgumentException("Meeting slug is required"); +// } + +// // The persisted meetingLink is the short URL produced by generateMeetingLink(), +// // i.e. "m=". Reconstruct it to look up the row. +// String shortLink = jitsiLink + "m=" + slug; +// VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); + +// if (params == null) { +// throw new Exception("No meeting found for slug: " + slug); +// } + +// // Note: we deliberately do NOT block on linkUsed=true here, because real +// // calls drop and the agent/beneficiary often have to rejoin. The linkUsed +// // flag is for reporting in updateCallStatus, not access control. Access +// // control comes from the JWT exp + the room claim. + +// String roomName = roomPrefix + slug; +// String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() +// ? params.getAgentName() +// : "Guest"; + +// String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail); + +// String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; +// logger.info("Resolved slug {} -> room {}", slug, roomName); +// return redirectUrl; +// } + @Override public String resolveMeetingLink(String slug) throws Exception { if (slug == null || slug.isEmpty()) { throw new IllegalArgumentException("Meeting slug is required"); } - // The persisted meetingLink is the short URL produced by generateMeetingLink(), - // i.e. "m=". Reconstruct it to look up the row. String shortLink = jitsiLink + "m=" + slug; VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); @@ -183,10 +250,10 @@ public String resolveMeetingLink(String slug) throws Exception { throw new Exception("No meeting found for slug: " + slug); } - // Note: we deliberately do NOT block on linkUsed=true here, because real - // calls drop and the agent/beneficiary often have to rejoin. The linkUsed - // flag is for reporting in updateCallStatus, not access control. Access - // control comes from the JWT exp + the room claim. + // ✅ ADD THIS — block re-entry after call ends + if (params.isLinkUsed()) { + throw new Exception("This meeting link has already been used and is no longer active."); + } String roomName = roomPrefix + slug; String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() @@ -194,8 +261,8 @@ public String resolveMeetingLink(String slug) throws Exception { : "Guest"; String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail); - String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; + logger.info("Resolved slug {} -> room {}", slug, roomName); return redirectUrl; } From 6e9edb868ef05bd4d90a03aae1fc5abd8ef2e343 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 20 Apr 2026 10:58:22 +0530 Subject: [PATCH 68/70] fix: update video call recording functionality --- .../videocall/VideoCallController.java | 45 ++++++++++++++-- .../videocall/VideoCallServiceImpl.java | 51 +++++++++++++++---- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index 94de8357..cda68817 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -89,16 +89,52 @@ public String sendVideoLink(@RequestBody String requestModel, HttpServletRequest } } +// @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") +// public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { +// OutputResponse response = new OutputResponse(); + +// try { +// if (requestModel.getMeetingLink() == null || requestModel.getCallStatus() == null) { +// throw new IllegalArgumentException("Meeting Link and Status are required"); +// } + +// String result = videoCallService.updateCallStatus(requestModel); + +// JSONObject responseObj = new JSONObject(); +// responseObj.put("status", "success"); +// responseObj.put("message", result); +// response.setResponse(responseObj.toString()); + +// } catch (IllegalArgumentException e) { +// logger.error("Validation error: " + e.getMessage(), e); +// return ResponseEntity.badRequest().body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); +// } catch (Exception e) { +// logger.error("updateCallStatus failed with error: " + e.getMessage(), e); +// response.setError(e); +// } + +// return ResponseEntity.ok(response.toString()); +// } + @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") -public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { +public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, + HttpServletRequest request) { OutputResponse response = new OutputResponse(); + logger.info("[updateCallStatus CONTROLLER] Received — meetingLink={}, callStatus={}, callDuration={}, modifiedBy={}, isLinkUsed={}", + requestModel.getMeetingLink(), + requestModel.getCallStatus(), + requestModel.getCallDuration(), + requestModel.getModifiedBy(), + requestModel.getIsLinkUsed()); try { if (requestModel.getMeetingLink() == null || requestModel.getCallStatus() == null) { + logger.error("[updateCallStatus CONTROLLER] Validation failed — meetingLink or callStatus is null"); throw new IllegalArgumentException("Meeting Link and Status are required"); } String result = videoCallService.updateCallStatus(requestModel); + logger.info("[updateCallStatus CONTROLLER] Service returned successfully"); JSONObject responseObj = new JSONObject(); responseObj.put("status", "success"); @@ -106,10 +142,11 @@ public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest re response.setResponse(responseObj.toString()); } catch (IllegalArgumentException e) { - logger.error("Validation error: " + e.getMessage(), e); - return ResponseEntity.badRequest().body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); + logger.error("[updateCallStatus CONTROLLER] Validation error: {}", e.getMessage(), e); + return ResponseEntity.badRequest() + .body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); } catch (Exception e) { - logger.error("updateCallStatus failed with error: " + e.getMessage(), e); + logger.error("[updateCallStatus CONTROLLER] Unexpected error: {}", e.getMessage(), e); response.setError(e); } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index d6d45009..9771dd61 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -150,17 +150,32 @@ public String sendMeetingLink(VideoCallRequest request) throws Exception { @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { - String meetingLink = callRequest.getMeetingLink(); + logger.info("[updateCallStatus] START — meetingLink={}, callStatus={}, callDuration={}, modifiedBy={}, isLinkUsed={}", + meetingLink, + callRequest.getCallStatus(), + callRequest.getCallDuration(), + callRequest.getModifiedBy(), + callRequest.getIsLinkUsed()); - VideoCallParameters videoCall = videoCallRepository.findByMeetingLink(meetingLink); - if (videoCall == null) { + // 1. Verify the row actually exists before attempting update + VideoCallParameters existing = videoCallRepository.findByMeetingLink(meetingLink); + if (existing == null) { + logger.error("[updateCallStatus] No row found in t_videocallparameter for meetingLink={}", meetingLink); throw new Exception("No meeting found for link: " + meetingLink); } + logger.info("[updateCallStatus] Found existing row — meetingID={}, currentStatus={}, currentLinkUsed={}, currentRecording={}", + existing.getMeetingID(), + existing.getCallStatus(), + existing.isLinkUsed(), + existing.getRecordingFileName()); + // 2. Derive the two fields the old query was missing boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); String recordingFileName = buildRecordingFileName(meetingLink); + logger.info("[updateCallStatus] Computed — linkUsed={}, recordingFileName={}", linkUsed, recordingFileName); + // 3. Single atomic JPQL UPDATE — sets ALL five fields in one DB round-trip int updateCount = videoCallRepository.updateCallStatusAndRecording( meetingLink, callRequest.getCallStatus(), @@ -169,19 +184,23 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { linkUsed, recordingFileName ); + logger.info("[updateCallStatus] JPQL updateCallStatusAndRecording affected {} row(s)", updateCount); if (updateCount == 0) { - throw new Exception("Failed to update the call status"); + logger.error("[updateCallStatus] Update affected 0 rows — possible meetingLink mismatch. meetingLink={}", meetingLink); + throw new Exception("Failed to update the call status — 0 rows affected"); } - // Refresh the entity to reflect DB state before serialising - videoCall = videoCallRepository.findByMeetingLink(meetingLink); - - logger.info("Call ended — link={}, isLinkUsed={}, recording={}", - meetingLink, linkUsed, recordingFileName); + // 4. Re-fetch AFTER the update so the returned JSON reflects what is now in the DB + VideoCallParameters updated = videoCallRepository.findByMeetingLink(meetingLink); + logger.info("[updateCallStatus] Post-update state — callStatus={}, callDuration={}, linkUsed={}, recordingFileName={}", + updated.getCallStatus(), + updated.getCallDuration(), + updated.isLinkUsed(), + updated.getRecordingFileName()); return OutputMapper.gsonWithoutExposeRestriction() - .toJson(videoCallMapper.videoCallToResponse(videoCall)); + .toJson(videoCallMapper.videoCallToResponse(updated)); } /** @@ -190,19 +209,29 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { * The short SMS link is "m=", so derive the room from the slug. */ private String buildRecordingFileName(String meetingLink) { + logger.info("[buildRecordingFileName] Input meetingLink={}", meetingLink); + if (meetingLink == null) { + logger.warn("[buildRecordingFileName] meetingLink is null — returning null"); return null; } + int idx = meetingLink.lastIndexOf("m="); if (idx < 0) { + logger.warn("[buildRecordingFileName] 'm=' marker not found in meetingLink={} — returning null", meetingLink); return null; } + String slug = meetingLink.substring(idx + 2); if (slug.isEmpty()) { + logger.warn("[buildRecordingFileName] slug is empty after 'm=' in meetingLink={} — returning null", meetingLink); return null; } + String roomName = roomPrefix + slug; - return roomName + "/" + roomName + ".mp4"; + String fileName = roomName + "/" + roomName + ".mp4"; + logger.info("[buildRecordingFileName] slug={}, roomName={}, fileName={}", slug, roomName, fileName); + return fileName; } // @Override From e86e24c54ecd2953966218a3d4b76231328e598d Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 20 Apr 2026 11:41:57 +0530 Subject: [PATCH 69/70] fix: remove unwanted codes --- .../videocall/VideoCallController.java | 27 ------- .../VideoCallParameterRepository.java | 3 - .../videocall/VideoCallServiceImpl.java | 73 +------------------ 3 files changed, 1 insertion(+), 102 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index cda68817..bf4f65a4 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -89,33 +89,6 @@ public String sendVideoLink(@RequestBody String requestModel, HttpServletRequest } } -// @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") -// public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { -// OutputResponse response = new OutputResponse(); - -// try { -// if (requestModel.getMeetingLink() == null || requestModel.getCallStatus() == null) { -// throw new IllegalArgumentException("Meeting Link and Status are required"); -// } - -// String result = videoCallService.updateCallStatus(requestModel); - -// JSONObject responseObj = new JSONObject(); -// responseObj.put("status", "success"); -// responseObj.put("message", result); -// response.setResponse(responseObj.toString()); - -// } catch (IllegalArgumentException e) { -// logger.error("Validation error: " + e.getMessage(), e); -// return ResponseEntity.badRequest().body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); -// } catch (Exception e) { -// logger.error("updateCallStatus failed with error: " + e.getMessage(), e); -// response.setError(e); -// } - -// return ResponseEntity.ok(response.toString()); -// } - @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { diff --git a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java index e9f81f44..7c9cbf26 100644 --- a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java +++ b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java @@ -22,14 +22,11 @@ package com.iemr.common.repository.videocall; -import java.util.List; - import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.data.jpa.repository.Query; import com.iemr.common.data.videocall.VideoCallParameters; -import com.iemr.common.model.videocall.VideoCallRequest; import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 9771dd61..825ab5cf 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -29,19 +29,12 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.sql.Timestamp; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.io.IOException; import com.iemr.common.data.videocall.VideoCallParameters; import com.iemr.common.mapper.videocall.VideoCallMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; import com.iemr.common.utils.JitsiJwtUtil; -import com.iemr.common.utils.config.ConfigProperties; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; import org.springframework.beans.factory.annotation.Value; @@ -116,38 +109,6 @@ public String sendMeetingLink(VideoCallRequest request) throws Exception { .replace("\\u0026", "&"); } -// @Override -// public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { -// VideoCallParameters videoCall = null; - -// VideoCallParameters requestEntity = videoCallMapper.updateRequestToVideoCall(callRequest); - -// videoCall = videoCallRepository.findByMeetingLink(requestEntity.getMeetingLink()); - -// int updateCount = videoCallRepository.updateCallStatusByMeetingLink( -// requestEntity.getMeetingLink(), -// requestEntity.getCallStatus(), -// requestEntity.getCallDuration(), -// requestEntity.getModifiedBy() -// ); - -// if (updateCount > 0) { -// // End-consultation: UI sends isLinkUsed=true; fall back to true for -// // backwards compatibility with older callers that didn't send the flag. -// boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); -// videoCall.setLinkUsed(linkUsed); -// videoCall.setRecordingFileName(buildRecordingFileName(requestEntity.getMeetingLink())); -// videoCallRepository.save(videoCall); - -// } else { -// throw new Exception("Failed to update the call status"); -// } - -// return OutputMapper.gsonWithoutExposeRestriction() -// .toJson(videoCallMapper.videoCallToResponse(videoCall)); -// } - - @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { String meetingLink = callRequest.getMeetingLink(); @@ -170,7 +131,7 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { existing.isLinkUsed(), existing.getRecordingFileName()); - // 2. Derive the two fields the old query was missing + // 2. Derive the two fields boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); String recordingFileName = buildRecordingFileName(meetingLink); logger.info("[updateCallStatus] Computed — linkUsed={}, recordingFileName={}", linkUsed, recordingFileName); @@ -234,38 +195,6 @@ private String buildRecordingFileName(String meetingLink) { return fileName; } -// @Override -// public String resolveMeetingLink(String slug) throws Exception { -// if (slug == null || slug.isEmpty()) { -// throw new IllegalArgumentException("Meeting slug is required"); -// } - -// // The persisted meetingLink is the short URL produced by generateMeetingLink(), -// // i.e. "m=". Reconstruct it to look up the row. -// String shortLink = jitsiLink + "m=" + slug; -// VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); - -// if (params == null) { -// throw new Exception("No meeting found for slug: " + slug); -// } - -// // Note: we deliberately do NOT block on linkUsed=true here, because real -// // calls drop and the agent/beneficiary often have to rejoin. The linkUsed -// // flag is for reporting in updateCallStatus, not access control. Access -// // control comes from the JWT exp + the room claim. - -// String roomName = roomPrefix + slug; -// String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() -// ? params.getAgentName() -// : "Guest"; - -// String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail); - -// String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; -// logger.info("Resolved slug {} -> room {}", slug, roomName); -// return redirectUrl; -// } - @Override public String resolveMeetingLink(String slug) throws Exception { if (slug == null || slug.isEmpty()) { From 4bfad242190f5e66beb1083633f60bf53d596463 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 20 Apr 2026 11:51:44 +0530 Subject: [PATCH 70/70] fix: coderabbit comments --- .../common/service/videocall/VideoCallServiceImpl.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 825ab5cf..0fb9acdb 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -76,9 +76,7 @@ public VideoCallServiceImpl() { @Override public String generateMeetingLink() { - logger.info("Jitsi Link: " + jitsiLink); meetingLink=jitsiLink+"m="+RandomStringUtils.randomAlphanumeric(8); - logger.info("Meeting link: " + meetingLink); return meetingLink; } @@ -112,7 +110,6 @@ public String sendMeetingLink(VideoCallRequest request) throws Exception { @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { String meetingLink = callRequest.getMeetingLink(); - logger.info("[updateCallStatus] START — meetingLink={}, callStatus={}, callDuration={}, modifiedBy={}, isLinkUsed={}", meetingLink, callRequest.getCallStatus(), callRequest.getCallDuration(), @@ -125,7 +122,6 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { logger.error("[updateCallStatus] No row found in t_videocallparameter for meetingLink={}", meetingLink); throw new Exception("No meeting found for link: " + meetingLink); } - logger.info("[updateCallStatus] Found existing row — meetingID={}, currentStatus={}, currentLinkUsed={}, currentRecording={}", existing.getMeetingID(), existing.getCallStatus(), existing.isLinkUsed(), @@ -134,7 +130,6 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { // 2. Derive the two fields boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); String recordingFileName = buildRecordingFileName(meetingLink); - logger.info("[updateCallStatus] Computed — linkUsed={}, recordingFileName={}", linkUsed, recordingFileName); // 3. Single atomic JPQL UPDATE — sets ALL five fields in one DB round-trip int updateCount = videoCallRepository.updateCallStatusAndRecording( @@ -154,7 +149,6 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { // 4. Re-fetch AFTER the update so the returned JSON reflects what is now in the DB VideoCallParameters updated = videoCallRepository.findByMeetingLink(meetingLink); - logger.info("[updateCallStatus] Post-update state — callStatus={}, callDuration={}, linkUsed={}, recordingFileName={}", updated.getCallStatus(), updated.getCallDuration(), updated.isLinkUsed(), @@ -170,7 +164,6 @@ public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { * The short SMS link is "m=", so derive the room from the slug. */ private String buildRecordingFileName(String meetingLink) { - logger.info("[buildRecordingFileName] Input meetingLink={}", meetingLink); if (meetingLink == null) { logger.warn("[buildRecordingFileName] meetingLink is null — returning null"); @@ -191,7 +184,6 @@ private String buildRecordingFileName(String meetingLink) { String roomName = roomPrefix + slug; String fileName = roomName + "/" + roomName + ".mp4"; - logger.info("[buildRecordingFileName] slug={}, roomName={}, fileName={}", slug, roomName, fileName); return fileName; } @@ -208,7 +200,6 @@ public String resolveMeetingLink(String slug) throws Exception { throw new Exception("No meeting found for slug: " + slug); } - // ✅ ADD THIS — block re-entry after call ends if (params.isLinkUsed()) { throw new Exception("This meeting link has already been used and is no longer active."); } @@ -221,7 +212,6 @@ public String resolveMeetingLink(String slug) throws Exception { String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail); String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; - logger.info("Resolved slug {} -> room {}", slug, roomName); return redirectUrl; }