From 7280c422e02eddc681dc5cf2f277639b07879e08 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 19 Jan 2026 13:22:40 +0530 Subject: [PATCH 1/7] 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 2/7] 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 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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=