From 9f23f7958b823e62a69da14abd750d6e48c5414c Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:33:22 +0530 Subject: [PATCH 1/5] Fix the Build Error (#400) * fix: build issue * fix: update the driver class --- src/main/environment/common_ci.properties | 4 ++-- src/main/environment/common_docker.properties | 4 ++-- src/main/environment/common_example.properties | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index f2b774a3..c4562362 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -3,12 +3,12 @@ spring.datasource.url=@env.DATABASE_URL@ spring.datasource.username=@env.DATABASE_USERNAME@ spring.datasource.password=@env.DATABASE_PASSWORD@ -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=@env.REPORTING_DATABASE_USERNAME@ secondary.datasource.password=@env.REPORTING_DATABASE_PASSWORD@ secondary.datasource.url=@env.REPORTING_DATABASE_URL@ -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=@env.KM_API_BASE_PROTOCOL@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index a5c633e4..50bcdf9c 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -3,12 +3,12 @@ spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=${REPORTING_DATABASE_USERNAME} secondary.datasource.password=${REPORTING_DATABASE_PASSWORD} secondary.datasource.url=${REPORTING_DATABASE_URL} -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=${KM_API_BASE_PROTOCOL} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index e3b5c031..70176d36 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -7,12 +7,12 @@ spring.datasource.password=1234 encDbUserName=zFlYsp9Z0s+lRvLM15A3g/Ba0w8VGs/1usuW7EsGF3k= encDbPass=JGGAGn5wTlrbTLUHY+5BzfBa0w8VGs/1usuW7EsGF3k= -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=root secondary.datasource.password=1234 secondary.datasource.url=jdbc:mysql://localhost:3306/db_reporting -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration @@ -42,6 +42,7 @@ genben-api=http://localhost:8092 send-sms=false sendSMSUrl = http://localhost:8083/sms/sendSMS source-address=AIDSHL +sms-consent-source-address= sms-username= sms-password= send-message-url= From 1c69049e6ba5110616f31534968f804a43c71b3c Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Mon, 4 May 2026 13:01:51 +0530 Subject: [PATCH 2/5] fix: add missing property for swagge (#401) --- src/main/resources/application-swagger.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index f73e6fd2..74ae2cbb 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -23,6 +23,7 @@ secondary.datasource.username= secondary.datasource.password= secondary.datasource.url=jdbc:h2:mem:reportingdb secondary.datasource.driver-class-name=org.h2.Driver +tempFilePath=temp springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true From 0da4823445b78fd20980d5d24fb6f4aef2cf2336 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Thu, 21 May 2026 14:56:26 +0530 Subject: [PATCH 3/5] fix: merge 3.6.2 to release 3.8.1 (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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> * add facilityData with employeeId in login response (#385) * 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> * feat: added the facilty in resonse * fix: login response change * feat: added the facilty in resonse * fix: logoin response * fix: login response --------- 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: Mithun James Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: vanitha1822 Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) * fix: property values are not fetching after deploying in linux server * feature for hindi translation for CG * 1097 Abandon call real time data (#365) * fix: integrate API for get disposition count * fix: respnse structure * fix: coderabbit comments * fix: fetch from .env (#369) * add otp_consent template key * fix welcome sms code * fix welcome sms code * fix: login response asha list misisng * fix: occupationID and educationID not saved during beneficiary update setDemographicDetails() was overwriting occupationName (already set correctly by the mapper from occupationID) with null when no occupation name string was present in the payload. Added null guards so the mapper's resolved name is preserved, and explicitly set occupationId/educationId from i_bendemographics to ensure the IDs always reach Identity-API. Co-Authored-By: Claude Sonnet 4.6 * Revert "fix: property values are not fetching after deploying in linux server" This reverts commit eb917b2eb4901874502a598bc46d751010304524. * fix: concurrent session logout not invalidating JWT in first system logOutUserFromConcurrentSession only cleaned up old-style Redis session keys but never added the displaced user's JWT to the denylist. Because JwtUserIdValidationFilter validates solely via JWT signature and the denylist, System 1's token remained valid and all APIs returned 200 after System 2 forced a concurrent login. Fix: store a username→JTI mapping in Redis at login time; during concurrent-session logout, look up the JTI and add it to the denylist and evict the user_ cache so the next request from System 1 is rejected with 401 and the frontend shows the session-expiry message. Co-Authored-By: Claude Sonnet 4.6 * fix: concurrent session logout not invalidating JWT on first system logOutUserFromConcurrentSession only cleaned up old-style Redis session keys but never added the displaced user's JWT to the denylist. Because JwtUserIdValidationFilter validates solely via JWT signature and the denylist, System 1's token remained valid and all APIs returned 200 after System 2 forced a concurrent login. The root serialization bug: redisTemplate value serializer is Jackson2JsonRedisSerializer, so storing a plain String JTI caused a deserialization failure on retrieval. Fixed by using the existing StringRedisTemplate bean for the jti: key operations. Fix: - Store username->JTI mapping via StringRedisTemplate at login (both userAuthenticate and superUserAuthenticate) - On concurrent-session logout, retrieve the JTI, add it to the denylist, evict user_ from User cache, and clean up jti: key - Add getAccessTokenExpiration() to JwtUtil to supply the TTL Co-Authored-By: Claude Sonnet 4.6 * fix(security): remove PII from JWT token for mobile logins (#413) Add generateSecureToken/generateSecureRefreshToken methods that use userId as sub instead of username. Mobile logins (okhttp User-Agent) use the secure token — web logins remain unchanged for backward compatibility. Other services will be migrated one by one. Co-authored-by: Claude Sonnet 4.6 * feat: add mobile to peersAtFacility and ashaList in facilityData login response (#417) Co-authored-by: Claude Sonnet 4.6 * Merge Releases 3.7.0 and 3.8.1 (#416) * implement translation in dynamic form * 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> * Restrict user when account is locked * 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: merge 3.7.0 to main * Video Consultation Functionality (#380) * Update application.properties * add column in create BeneficiaryModel * 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 * update language * update language * Downgrade version from 3.6.1 to 3.6.0 * 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 * Remove empty line in application.properties * fix:signature check for mmu * Update application.properties * Update application.properties * fix: retrive any user without deleted * implement state wise hide un hide form fields * implement state wise hide un hide form fields * implement state wise hide un hide form fields * enhance welcome sms code * fix hide unhide form issue * docs: add DeepWiki badge and documentation link * Add DeepWiki badge to README Added DeepWiki badge to README for better visibility. * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * 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 * 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 * 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 * 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 * 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> * fix: video consultation functionality * fix: pom version update * fix: add cti-server-ip * fix: comment unwanted code * fix: update videocall url property * fix: update cti-server-ip * docs: add CLAUDE.md for Claude Code guidance Co-Authored-By: Claude Opus 4.6 (1M context) * fix: KM issue * fix: KM issue * fix: remove unwanted imports * fix: conflicts * fix: update the temp path * Fix the OpenKM Issue (#389) * fix: remove km in application.properties * fix: update all the properties to fetch from env * fix: update path * fix: KM issue * fix: get file from km * fix: build issue * fix: build issue * fix: remove unwanted imports * fix: build issue * fix: remove commented line * Enable KM configuration in common_example.properties Uncomment KM configuration properties for OpenKM. * Fix ConfigProperties to resolve env variable placeholders via Spring Environment (#390) Co-authored-by: Claude Opus 4.6 (1M context) * fix: update sms issue * fix: build issue * fix: update condition * fix: edit ben issue * fix: phone number issue for sms * fix: update the url with jwt token * fix: jitsi authorization issue * fix: skip auth * fix: hash key updation * fix: jwt type in header for authorization * fix: update file path * fix: vc recording path updation * fix: update video call recording functionality * fix: remove unwanted codes * fix: coderabbit comments --------- Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Saurav Mishra Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: vishwab1 Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: DurgaPrasad-54 Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Vaishnav Bhosale Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH * Fix the Build Issue (#397) * fix: build issue * fix: build issue * fix: merge with main * 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 the End Consultation Call for VC (#407) * fix: end the consultation on clicking the end meeting button * fix: end call * fix: the build issue * fix: the issue in agent-token * fix: remove slug (#414) * fix: build issue --------- Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: vishwab1 Co-authored-by: DurgaPrasad-54 Co-authored-by: Vaishnav Bhosale Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH * AMM-118: Add admin account lock controls (#332) Co-authored-by: Varun Deep Saini * Fix the OpenKM Issue (#389) * fix: remove km in application.properties * fix: update all the properties to fetch from env * fix: update path * fix: KM issue * fix: get file from km * fix: build issue * fix: build issue * fix: remove unwanted imports * fix: build issue * fix: remove commented line * Enable KM configuration in common_example.properties Uncomment KM configuration properties for OpenKM. --------- 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: Mithun James Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: vanitha1822 Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH Co-authored-by: Saurav Mishra Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: DurgaPrasad-54 Co-authored-by: Vaishnav Bhosale Co-authored-by: Varun Deep Saini Co-authored-by: Varun Deep Saini --- pom.xml | 2 +- src/main/environment/common_ci.properties | 13 +- src/main/environment/common_docker.properties | 12 +- .../environment/common_example.properties | 16 +- .../com/iemr/common/CommonApplication.java | 2 + .../iemr/common/config/InterceptorConfig.java | 3 +- .../config/encryption/SecurePassword.java | 2 - .../BeneficiaryRegistrationController.java | 4 + ...omputerTelephonyIntegrationController.java | 18 +- .../dynamicForm/DynamicFormController.java | 2 + .../controller/users/IEMRAdminController.java | 203 ++++++++- .../controller/version/VersionController.java | 2 + .../videocall/VideoCallController.java | 129 +++++- .../data/cti/DispositionCountRequest.java | 11 + .../data/cti/DispositionCountResponse.java | 43 ++ .../common/data/dynamic_from/FormField.java | 3 + .../data/dynamic_from/FormFieldOption.java | 34 ++ .../common/data/translation/Translation.java | 1 + .../data/users/AshaSupervisorMapping.java | 34 ++ .../java/com/iemr/common/data/users/User.java | 11 + .../data/videocall/VideoCallParameters.java | 25 ++ .../dto/dynamicForm/FieldResponseDTO.java | 2 +- .../mapper/videocall/VideoCallMapper.java | 23 +- .../model/beneficiary/BeneficiaryModel.java | 5 +- .../common/model/user/LoginResponseModel.java | 1 + .../model/videocall/UpdateCallRequest.java | 29 +- .../model/videocall/UpdateCallResponse.java | 23 + .../model/videocall/VideoCallRequest.java | 22 + .../dynamic_form/FieldRepository.java | 6 + .../FormFieldOptionRepository.java | 14 + .../translation/TranslationRepo.java | 1 + .../users/AshaSupervisorLoginRepo.java | 14 + .../repository/users/FacilityLoginRepo.java | 98 +++++ .../users/IEMRUserRepositoryCustom.java | 2 +- .../VideoCallParameterRepository.java | 52 ++- .../IEMRSearchUserServiceImpl.java | 2 + .../IdentityBeneficiaryServiceImpl.java | 11 + .../RegisterBenificiaryServiceImpl.java | 52 ++- .../BeneficiaryOTPHandlerImpl.java | 3 +- .../iemr/common/service/cti/CTIService.java | 2 + .../common/service/cti/CTIServiceImpl.java | 74 +++- .../dynamicForm/FormMasterServiceImpl.java | 72 +++- .../service/feedback/FeedbackServiceImpl.java | 19 +- .../KMFileManagerServiceImpl.java | 1 + .../NHM_DashboardServiceImpl.java | 8 +- .../notification/NotificationServiceImpl.java | 19 +- .../service/scheme/SchemeServiceImpl.java | 24 +- .../service/services/CommonServiceImpl.java | 25 +- .../common/service/sms/SMSServiceImpl.java | 38 +- .../users/AshaSupervisorLoginService.java | 225 ++++++++++ .../service/users/IEMRAdminUserService.java | 8 +- .../users/IEMRAdminUserServiceImpl.java | 399 ++++++++++++------ .../service/videocall/VideoCallService.java | 49 ++- .../videocall/VideoCallServiceImpl.java | 197 ++++++--- .../WelcomeBenificarySmsServiceImpl.java | 85 ++-- .../com/iemr/common/utils/IEMRApplBeans.java | 12 +- .../com/iemr/common/utils/JitsiJwtUtil.java | 111 +++++ .../common/utils/JwtAuthenticationUtil.java | 4 - .../utils/JwtUserIdValidationFilter.java | 26 +- .../java/com/iemr/common/utils/JwtUtil.java | 25 ++ .../common/utils/config/ConfigProperties.java | 16 +- .../utils/http/HTTPRequestInterceptor.java | 1 + .../utils/km/openkm/OpenKMServiceImpl.java | 57 +-- src/main/resources/application.properties | 28 +- .../videocall/VideoCallControllerTest.java | 37 ++ .../videocall/VideoCallServiceImplTest.java | 71 ++++ .../iemr/common/utils/JitsiJwtUtilTest.java | 144 +++++++ 67 files changed, 2293 insertions(+), 414 deletions(-) create mode 100644 src/main/java/com/iemr/common/data/cti/DispositionCountRequest.java create mode 100644 src/main/java/com/iemr/common/data/cti/DispositionCountResponse.java create mode 100644 src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java create mode 100644 src/main/java/com/iemr/common/data/users/AshaSupervisorMapping.java create mode 100644 src/main/java/com/iemr/common/repository/dynamic_form/FormFieldOptionRepository.java create mode 100644 src/main/java/com/iemr/common/repository/users/AshaSupervisorLoginRepo.java create mode 100644 src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java create mode 100644 src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java create mode 100644 src/main/java/com/iemr/common/utils/JitsiJwtUtil.java create mode 100644 src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java diff --git a/pom.xml b/pom.xml index 3250086b..b4e8c0d9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.0 + 3.8.1 war Common-API diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index c4562362..547e069f 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -190,10 +190,19 @@ captcha.enable-captcha=@env.ENABLE_CAPTCHA@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ -video-call-url=@env.VIDEO_CALL_URL@ -jibri.output.path=@env.JIBRI_OUTPUT_PATH@ +# Jitsi configuration +videocall.url=@env.VIDEO_CALL_URL@ video.recording.path=@env.VIDEO_RECORDING_PATH@ +# Jitsi JWT (prosody token-auth) +jitsi.app.id=@env.JITSI_APP_ID@ +jitsi.app.secret=@env.JITSI_APP_SECRET@ +jitsi.domain=@env.JITSI_DOMAIN@ +jitsi.sub=@env.JITSI_SUB@ +jitsi.token.ttl.seconds=@env.JITSI_TOKEN_TTL_SECONDS@ +jitsi.room.prefix=@env.JITSI_ROOM_PREFIX@ +jitsi.default.user.email=@env.JITSI_DEFAULT_USER_EMAIL@ + platform.feedback.ratelimit.enabled=@env.PLATFORM_FEEDBACK_RATELIMIT_ENABLED@ platform.feedback.ratelimit.pepper=@env.PLATFORM_FEEDBACK_RATELIMIT_PEPPER@ platform.feedback.ratelimit.trust-forwarded-for=@env.PLATFORM_FEEDBACK_RATELIMIT_TRUST_FORWARDED_FOR@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 50bcdf9c..064090b3 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -192,10 +192,18 @@ firebase.enabled=${FIREBASE_ENABLE} firebase.credential-file=${FIREBASE_CREDENTIAL} -video-call-url=${VIDEO_CALL_URL} -jibri.output.path={JIBRI_OUTPUT_PATH} +videocall.url=${VIDEO_CALL_URL} video.recording.path={VIDEO_RECORDING_PATH} +# Jitsi JWT (prosody token-auth) +jitsi.app.id=${JITSI_APP_ID} +jitsi.app.secret=${JITSI_APP_SECRET} +jitsi.domain=${JITSI_DOMAIN} +jitsi.sub=${JITSI_SUB} +jitsi.token.ttl.seconds=${JITSI_TOKEN_TTL_SECONDS} +jitsi.room.prefix=${JITSI_ROOM_PREFIX} +jitsi.default.user.email=${JITSI_DEFAULT_USER_EMAIL} + # Platform Feedback module platform.feedback.ratelimit.enabled=${PLATFORM_FEEDBACK_RATELIMIT_ENABLED} platform.feedback.ratelimit.pepper=${PLATFORM_FEEDBACK_RATELIMIT_PEPPER} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 70176d36..b37ab216 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -25,7 +25,7 @@ km-root-path=/okm:personal/users/ km-guest-user=guest km-guest-password=guest -tempFilePath=/opt/openkm +tempFilePath=/tmp # CTI Config cti-server-ip=10.208.122.99 @@ -201,9 +201,9 @@ grievanceAllocationRetryConfiguration=3 logging.path=logs/ logging.file.name=logs/common-api.log -video-call-url=https://vc.piramalswasthya.org/? -jibri.output.path=/srv/jibri/recordings -video.recording.path=/srv/recordings +# Jitsi configuration +videocall.url=https://vc.piramalswasthya.org/? +video.recording.path=/opt/recordings captcha.secret-key= captcha.verify-url= https://challenges.cloudflare.com/turnstile/v0/siteverify @@ -234,3 +234,11 @@ otp.ratelimit.day-limit=20 ### generate Beneficiary IDs URL generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs + +JITSI_APP_ID=piramal_vc +JITSI_APP_SECRET=5b9883418be6f228ffe3ceaa74dd3d3b91737733a4a85c5e82fc584ad449850b +JITSI_DOMAIN=vc.piramalswasthya.org +JITSI_SUB=meet.jitsi +JITSI_TOKEN_TTL_SECONDS=3600 +JITSI_ROOM_PREFIX=piramal-meeting- +JITSI_DEFAULT_USER_EMAIL=admin@piramalswasthya.org diff --git a/src/main/java/com/iemr/common/CommonApplication.java b/src/main/java/com/iemr/common/CommonApplication.java index e4a59994..45d61800 100644 --- a/src/main/java/com/iemr/common/CommonApplication.java +++ b/src/main/java/com/iemr/common/CommonApplication.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; @@ -40,6 +41,7 @@ @SpringBootApplication @EnableScheduling +@EnableAsync(proxyTargetClass = true) public class CommonApplication extends SpringBootServletInitializer { @Bean diff --git a/src/main/java/com/iemr/common/config/InterceptorConfig.java b/src/main/java/com/iemr/common/config/InterceptorConfig.java index a321eb08..8a45482a 100644 --- a/src/main/java/com/iemr/common/config/InterceptorConfig.java +++ b/src/main/java/com/iemr/common/config/InterceptorConfig.java @@ -36,7 +36,8 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(requestInterceptor); + registry.addInterceptor(requestInterceptor) + .excludePathPatterns("/video-consultation/resolve", "**/video-consultation/resolve"); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java index 15463b7a..95cdd7f3 100644 --- a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java +++ b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java @@ -26,10 +26,8 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; - import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; - import org.springframework.stereotype.Service; @Service 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 67f57981..e6df1980 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -37,6 +37,7 @@ 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.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -74,6 +75,8 @@ import com.iemr.common.service.userbeneficiarydata.TitleService; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; +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; @@ -212,6 +215,7 @@ public String createBeneficiary( OutputResponse response = new OutputResponse(); logger.info("Create beneficiary request " + beneficiaryModel); + logger.info("Common-api Call: Create bene"); try { response.setResponse(registerBenificiaryService.save(beneficiaryModel, httpRequest)); diff --git a/src/main/java/com/iemr/common/controller/cti/ComputerTelephonyIntegrationController.java b/src/main/java/com/iemr/common/controller/cti/ComputerTelephonyIntegrationController.java index 711019eb..0cf01777 100644 --- a/src/main/java/com/iemr/common/controller/cti/ComputerTelephonyIntegrationController.java +++ b/src/main/java/com/iemr/common/controller/cti/ComputerTelephonyIntegrationController.java @@ -21,7 +21,6 @@ */ package com.iemr.common.controller.cti; - import javax.ws.rs.core.MediaType; import org.slf4j.Logger; @@ -522,4 +521,21 @@ public String getIVRSPathDetails(@Param("{\"agent_id\":\"Integer\"}") @RequestBo logger.info("getIVRSPathDetails sending response " + response); return response.toString(); } + + @Operation(summary = "Get disposition count for campaign") + @RequestMapping(value = "/getDispositionCount", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String getDispositionCount(@RequestBody String request, HttpServletRequest serverRequest) { + OutputResponse response = new OutputResponse(); + try { + String remoteAddress = serverRequest.getHeader("X-FORWARDED-FOR"); + if (remoteAddress == null || remoteAddress.trim().length() == 0) { + remoteAddress = serverRequest.getRemoteAddr(); + } + response = ctiService.getDispositionCount(request, remoteAddress); + } catch (Exception e) { + logger.error("getDispositionCount failed with error " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } } 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 62bf7e7c..c24651c6 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -97,4 +97,6 @@ public ResponseEntity> getStructuredForm(@PathVariable String for } + + } 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 554500f3..2ae06257 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -56,6 +57,7 @@ import com.iemr.common.model.user.ForceLogoutRequestModel; import com.iemr.common.model.user.LoginRequestModel; import com.iemr.common.service.recaptcha.CaptchaValidationService; +import com.iemr.common.service.users.AshaSupervisorLoginService; import com.iemr.common.service.users.IEMRAdminUserService; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; @@ -77,6 +79,7 @@ @RequestMapping("/user") @RestController public class IEMRAdminController { + private static final String USER_ID_FIELD = "userId"; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); @@ -94,6 +97,8 @@ public class IEMRAdminController { private CookieUtil cookieUtil; @Autowired private RedisTemplate redisTemplate; + @Autowired + private StringRedisTemplate stringRedisTemplate; private AESUtil aesUtil; @@ -117,6 +122,9 @@ public void setSessionObject(SessionObject sessionObject) { @Autowired SecurePassword securePassword; + @Autowired + private AshaSupervisorLoginService ashaSupervisorLoginService; + @Operation(summary = "New user authentication") @RequestMapping(value = "/userAuthenticateNew", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON) public String userAuthenticateNew( @@ -186,15 +194,26 @@ public String userAuthenticate( String jwtToken = null; String refreshToken = null; if (mUser.size() == 1) { - jwtToken = jwtUtil.generateToken(m_User.getUserName(), mUser.get(0).getUserID().toString()); + String userIdStr = mUser.get(0).getUserID().toString(); + jwtToken = isMobile + ? jwtUtil.generateSecureToken(userIdStr) + : jwtUtil.generateToken(m_User.getUserName(), userIdStr); User user = new User(); // Assuming the Users class exists user.setUserID(mUser.get(0).getUserID()); user.setUserName(mUser.get(0).getUserName()); logger.info("UserAgentUtil isMobile : " + isMobile); + // Store username -> JTI mapping so concurrent-session logout can denylist this token + stringRedisTemplate.opsForValue().set( + "jti:" + m_User.getUserName().trim().toLowerCase(), + jwtUtil.getJtiFromToken(jwtToken) + "|" + mUser.get(0).getUserID(), + jwtUtil.getAccessTokenExpiration(), + TimeUnit.MILLISECONDS + ); + if (isMobile) { - refreshToken = jwtUtil.generateRefreshToken(m_User.getUserName(), user.getUserID().toString()); + refreshToken = jwtUtil.generateSecureRefreshToken(user.getUserID().toString()); logger.debug("Refresh token generated successfully for user: {}", user.getUserName()); String jti = jwtUtil.getJtiFromToken(refreshToken); redisTemplate.opsForValue().set( @@ -232,6 +251,46 @@ public String userAuthenticate( responseObj = iemrAdminUserServiceImpl.generateKeyAndValidateIP(responseObj, remoteAddress, request.getRemoteHost()); + // Facility data for ALL users - common pattern, empty if not applicable + try { + if (mUser.size() == 1) { + User loggedInUser = mUser.get(0); + String userRoleName = ""; + if (loggedInUser.getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : loggedInUser.getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRoleName = usrm.getM_Role().getRoleName(); + break; + } + } + } + JSONObject facilityData = ashaSupervisorLoginService + .buildFacilityLoginData(loggedInUser.getUserID(), userRoleName); + + // User details + JSONObject userObj = new JSONObject(); + userObj.put("userId", loggedInUser.getUserID()); + userObj.put("employeeId", loggedInUser.getEmployeeID() != null ? loggedInUser.getEmployeeID() : JSONObject.NULL); + userObj.put("role", userRoleName); + String first = loggedInUser.getFirstName() != null ? loggedInUser.getFirstName() : ""; + String last = loggedInUser.getLastName() != null ? loggedInUser.getLastName() : ""; + userObj.put("fullName", (first + " " + last).trim()); + + JSONObject demographics = new JSONObject(); + String genderName = ashaSupervisorLoginService.getGenderName(loggedInUser.getGenderID()); + demographics.put("gender", genderName != null ? genderName : JSONObject.NULL); + demographics.put("dob", loggedInUser.getdOB() != null ? loggedInUser.getdOB().toString() : JSONObject.NULL); + demographics.put("mobile", loggedInUser.getEmergencyContactNo() != null ? loggedInUser.getEmergencyContactNo() : JSONObject.NULL); + demographics.put("email", loggedInUser.getEmailID() != null ? loggedInUser.getEmailID() : JSONObject.NULL); + userObj.put("demographics", demographics); + + facilityData.put("user", userObj); + responseObj.put("facilityData", facilityData); + } + } catch (Exception e) { + logger.error("Error fetching facility login data: " + e.getMessage(), e); + } + // Add tokens to response for mobile if (isMobile && !mUser.isEmpty()) { responseObj.put("jwtToken", jwtToken); @@ -279,6 +338,16 @@ public ResponseEntity refreshToken(@RequestBody Map request) String userId = claims.get("userId", String.class); User user = iemrAdminUserServiceImpl.getUserById(Long.parseLong(userId)); + // validate if user account is locked or de-activated + if(user.getDeleted()){ + logger.warn("Your account is locked or de-activated. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is locked or de-activated. Please contact administrator."); + } + if(user.getStatusID()>2){ + logger.warn("Your account is not active. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is not active. Please contact administrator."); + } + // Validate that the user still exists and is active if (user == null) { logger.warn("Token validation failed: user not found for userId in token."); @@ -343,6 +412,19 @@ public String logOutUserFromConcurrentSession( if (previousTokenFromRedis != null) { deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); + + // Denylist the active JWT so System 1's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + stringRedisTemplate.delete("jti:" + usernameKey); + } + response.setResponse("User successfully logged out"); } else{ logger.error("Unable to fetch session from redis"); @@ -478,8 +560,16 @@ public String superUserAuthenticate( isMobile = UserAgentUtil.isMobileDevice(userAgent); logger.info("UserAgentUtil isMobile : " + isMobile); + // Store username -> JTI mapping so concurrent-session logout can denylist this token + stringRedisTemplate.opsForValue().set( + "jti:" + m_User.getUserName().trim().toLowerCase(), + jwtUtil.getJtiFromToken(jwtToken) + "|" + mUser.getUserID(), + jwtUtil.getAccessTokenExpiration(), + TimeUnit.MILLISECONDS + ); + if (isMobile) { - refreshToken = jwtUtil.generateRefreshToken(m_User.getUserName(), user.getUserID().toString()); + refreshToken = jwtUtil.generateSecureRefreshToken(user.getUserID().toString()); logger.debug("Refresh token generated successfully for user: {}", user.getUserName()); String jti = jwtUtil.getJtiFromToken(refreshToken); redisTemplate.opsForValue().set( @@ -583,6 +673,13 @@ public String getLoginResponse(HttpServletRequest request) { throw new IEMRException("Authentication failed. Please log in again."); } + // Validate the token first + Claims claims = jwtUtil.validateToken(jwtToken); + if (claims == null) { + logger.warn("Authentication failed: invalid or expired token."); + throw new IEMRException("Authentication failed. Please log in again."); + } + // Extract user ID from the JWT token String userId = jwtUtil.getUserIdFromToken(jwtToken); @@ -1248,4 +1345,104 @@ public ResponseEntity checkUserDetails(@PathVariable("userName") String userN } } + + @Operation(summary = "Lock user account") + @PostMapping(value = "/lockUserAccount", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String lockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean locked = iemrAdminUserServiceImpl.lockUserAccount(userId); + response.setResponse(locked ? "User account successfully locked" : "User account was already locked"); + } catch (Exception e) { + logger.error("Error locking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Unlock user account locked due to failed login attempts") + @PostMapping(value = "/unlockUserAccount", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String unlockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean unlocked = iemrAdminUserServiceImpl.unlockUserAccount(userId); + response.setResponse(unlocked ? "User account successfully unlocked" : "User account was not locked"); + } catch (Exception e) { + logger.error("Error unlocking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get user account lock status") + @PostMapping(value = "/getUserLockStatus", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String getUserLockStatus(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + String lockStatusJson = iemrAdminUserServiceImpl.getUserLockStatusJson(userId); + response.setResponse(lockStatusJson); + } catch (Exception e) { + logger.error("Error getting user lock status: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + private Long parseUserIdFromRequest(String request) throws IEMRException { + try { + JsonObject requestObj = JsonParser.parseString(request).getAsJsonObject(); + if (!requestObj.has(USER_ID_FIELD) || requestObj.get(USER_ID_FIELD).isJsonNull()) { + throw new IEMRException(USER_ID_FIELD + " is required"); + } + JsonElement userIdElement = requestObj.get(USER_ID_FIELD); + if (!userIdElement.isJsonPrimitive() || !userIdElement.getAsJsonPrimitive().isNumber()) { + throw new IEMRException(USER_ID_FIELD + " must be a number"); + } + return userIdElement.getAsLong(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to parse {} from request: {}", USER_ID_FIELD, e.getMessage()); + throw new IEMRException("Invalid request body"); + } + } + + private Long getAuthenticatedUserId(HttpServletRequest httpRequest) throws IEMRException { + String authorization = httpRequest.getHeader("Authorization"); + if (authorization != null && authorization.contains("Bearer ")) { + authorization = authorization.replace("Bearer ", ""); + } + if (authorization == null || authorization.isEmpty()) { + throw new IEMRException("Authentication required"); + } + try { + String sessionJson = sessionObject.getSessionObject(authorization); + if (sessionJson == null || sessionJson.isEmpty()) { + throw new IEMRException("Session expired. Please log in again."); + } + JSONObject session = new JSONObject(sessionJson); + return session.getLong("userID"); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Authentication failed while extracting user ID: {}", e.getMessage()); + throw new IEMRException("Authentication failed"); + } + } + + private void validateAdminPrivileges(Long userId) throws IEMRException { + if (!iemrAdminUserServiceImpl.hasAdminPrivileges(userId)) { + logger.warn("Unauthorized access attempt by userId: {}", userId); + throw new IEMRException("Access denied. Admin privileges required."); + } + } } diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 1b02ee59..69b067db 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -75,6 +75,8 @@ public ResponseEntity> versionInformation() { return ResponseEntity.ok(response); } + + private Properties loadGitProperties() throws IOException { Properties properties = new Properties(); try (InputStream input = getClass().getClassLoader() diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index 8eb2a3ad..436b80a9 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -1,5 +1,28 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.controller.videocall; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -12,19 +35,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.service.videocall.VideoCallService; import com.iemr.common.utils.response.OutputResponse; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.web.bind.annotation.RequestBody; +import jakarta.servlet.http.HttpServletRequest; @RestController @RequestMapping(value = "/video-consultation") @@ -67,15 +90,24 @@ public String sendVideoLink(@RequestBody String requestModel, HttpServletRequest } @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") -public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { +public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, + HttpServletRequest request) { OutputResponse response = new OutputResponse(); + logger.info("[updateCallStatus CONTROLLER] Received — meetingLink={}, callStatus={}, callDuration={}, modifiedBy={}, isLinkUsed={}", + requestModel.getMeetingLink(), + requestModel.getCallStatus(), + requestModel.getCallDuration(), + requestModel.getModifiedBy(), + requestModel.getIsLinkUsed()); try { if (requestModel.getMeetingLink() == null || requestModel.getCallStatus() == null) { + logger.error("[updateCallStatus CONTROLLER] Validation failed — meetingLink or callStatus is null"); throw new IllegalArgumentException("Meeting Link and Status are required"); } String result = videoCallService.updateCallStatus(requestModel); + logger.info("[updateCallStatus CONTROLLER] Service returned successfully"); JSONObject responseObj = new JSONObject(); responseObj.put("status", "success"); @@ -83,16 +115,93 @@ public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest re response.setResponse(responseObj.toString()); } catch (IllegalArgumentException e) { - logger.error("Validation error: " + e.getMessage(), e); - return ResponseEntity.badRequest().body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); + logger.error("[updateCallStatus CONTROLLER] Validation error: {}", e.getMessage(), e); + return ResponseEntity.badRequest() + .body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); } catch (Exception e) { - logger.error("updateCallStatus failed with error: " + e.getMessage(), e); + logger.error("[updateCallStatus CONTROLLER] Unexpected error: {}", e.getMessage(), e); response.setError(e); } return ResponseEntity.ok(response.toString()); } +/** + * Returns a moderator JWT URL for the agent so they can use "End Meeting for All". + * Called by the frontend after the meeting link is generated. + */ +@PostMapping(value = "/agent-token", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") +public ResponseEntity> generateAgentToken(@RequestBody Map body) { + Map response = new HashMap<>(); + try { + String slug = body.get("slug"); + String agentName = body.get("agentName"); + String agentEmail = body.get("agentEmail"); + + if (slug == null || slug.isEmpty()) { + response.put("error", "slug is required"); + return ResponseEntity.badRequest().body(response); + } + + String agentUrl = videoCallService.generateAgentToken(slug, agentName, agentEmail); + response.put("agentMeetingUrl", agentUrl); + + // Parse roomName and jwt out of the URL so the frontend can pass them + // directly to JitsiMeetExternalAPI without re-parsing the URL itself. + // URL format: https:///?jwt= + int jwtIdx = agentUrl.lastIndexOf("?jwt="); + if (jwtIdx > 0) { + String jwt = agentUrl.substring(jwtIdx + 5); + String pathPart = agentUrl.substring(0, jwtIdx); + String roomName = pathPart.substring(pathPart.lastIndexOf('/') + 1); + response.put("roomName", roomName); + response.put("jwt", jwt); + } + + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + response.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + logger.error("generateAgentToken failed: {}", e.getMessage(), e); + response.put("error", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } +} + +/** + * Public redirect endpoint hit when a beneficiary clicks the short SMS link. + * + * Flow: + * 1. Jitsi host nginx receives "https://vc.piramalswasthya.org/?m=<slug>" + * and proxies/redirects it to this endpoint. + * 2. This endpoint looks up the slug, mints a fresh Jitsi JWT bound to the + * room and the agent, and 302-redirects the browser to the full Jitsi URL + * "https://vc.piramalswasthya.org/<room>?jwt=<token>". + * 3. The Jitsi server enforces the JWT (prosody token-auth) and admits the user. + * + * Intentionally NOT guarded by Authorization header - the SMS recipient is on + * a phone browser and has no app session. Access control is the JWT itself + * plus the slug being unguessable and the meeting row existing. + */ +@GetMapping(value = "/resolve") +public ResponseEntity resolveMeetingLink(@RequestParam("m") String slug) { + try { + String redirectUrl = videoCallService.resolveMeetingLink(slug); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } catch (IllegalArgumentException e) { + logger.warn("resolveMeetingLink rejected: {}", e.getMessage()); + return ResponseEntity.badRequest().build(); + } catch (Exception e) { + logger.error("resolveMeetingLink failed for slug={}: {}", slug, e.getMessage(), e); - + // Distinguish "link expired" from "not found" with a 410 Gone + if (e.getMessage() != null && e.getMessage().contains("already been used")) { + return ResponseEntity.status(HttpStatus.GONE).build(); // 410 + } + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); +} +} } diff --git a/src/main/java/com/iemr/common/data/cti/DispositionCountRequest.java b/src/main/java/com/iemr/common/data/cti/DispositionCountRequest.java new file mode 100644 index 00000000..8c72d323 --- /dev/null +++ b/src/main/java/com/iemr/common/data/cti/DispositionCountRequest.java @@ -0,0 +1,11 @@ +package com.iemr.common.data.cti; +import lombok.Data; + +@Data +public class DispositionCountRequest { + private String transaction_id = "CTI_GET_DISP_COUNT"; + private String campaign_name; + private String disposition; + private String date; + private String enc_flag; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/data/cti/DispositionCountResponse.java b/src/main/java/com/iemr/common/data/cti/DispositionCountResponse.java new file mode 100644 index 00000000..858b7e00 --- /dev/null +++ b/src/main/java/com/iemr/common/data/cti/DispositionCountResponse.java @@ -0,0 +1,43 @@ +package com.iemr.common.data.cti; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.gson.annotations.Expose; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DispositionCountResponse { + @Expose + private String status; + + @Expose + private Integer code; + + @Expose + private String failure_reason; + + @Expose + private Object data; + + // Inner class for campaign data + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class CampaignData { + @Expose + private String campaign_name; + + @Expose + private List dispositions; + } + + // Inner class for disposition detail + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class DispositionDetail { + @Expose + private String call_status_disposition; + + @Expose + private String disposition_count; + } +} 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 1b195db9..08534239 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 @@ -62,6 +62,9 @@ public class FormField { @Column(name = "created_at") private LocalDateTime createdAt = LocalDateTime.now(); + @Column(name = "option_key") + private String optionKey; + } diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java new file mode 100644 index 00000000..8cfeb0de --- /dev/null +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java @@ -0,0 +1,34 @@ +package com.iemr.common.data.dynamic_from; + +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Table(name = "form_field_options") +@Data +public class FormFieldOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "option_key") + private String optionKey; + + @Column(name = "value") + private String value; + + @Column(name = "label_en") + private String labelEn; + + @Column(name = "label_hi") + private String labelHi; + + @Column(name = "label_as") + private String labelAs; + + @Column(name = "sort_order") + private Integer sortOrder; + + // getters/setters +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/data/translation/Translation.java b/src/main/java/com/iemr/common/data/translation/Translation.java index 0dad116d..52cb8027 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -22,4 +22,5 @@ public class Translation { private String assameseTranslation; @Column(name = "is_active") private Boolean isActive; + } diff --git a/src/main/java/com/iemr/common/data/users/AshaSupervisorMapping.java b/src/main/java/com/iemr/common/data/users/AshaSupervisorMapping.java new file mode 100644 index 00000000..9aa38db6 --- /dev/null +++ b/src/main/java/com/iemr/common/data/users/AshaSupervisorMapping.java @@ -0,0 +1,34 @@ +package com.iemr.common.data.users; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "asha_supervisor_mapping") +@Data +@NoArgsConstructor +public class AshaSupervisorMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "supervisorUserID") + private Integer supervisorUserID; + + @Column(name = "ashaUserID") + private Integer ashaUserID; + + @Column(name = "facilityID") + private Integer facilityID; + + @Column(name = "deleted", insertable = false, updatable = true) + private Boolean deleted; +} diff --git a/src/main/java/com/iemr/common/data/users/User.java b/src/main/java/com/iemr/common/data/users/User.java index 275b0ec6..83e5ac27 100644 --- a/src/main/java/com/iemr/common/data/users/User.java +++ b/src/main/java/com/iemr/common/data/users/User.java @@ -132,6 +132,9 @@ public class User implements Serializable { @Expose private Status m_status; + @Expose + @Column(name = "EmployeeID") + private String employeeID; @Expose @Column(name = "AadhaarNo") private String aadhaarNo; @@ -213,6 +216,10 @@ public class User implements Serializable { @Column(name = "dhistoken") private String dhistoken; + @Expose + @Column(name = "lock_timestamp") + private Timestamp lockTimestamp; + /* * protected User() { } */ @@ -515,6 +522,10 @@ public String getAgentID() { return agentID; } + public String getEmployeeID() { + return employeeID; + } + public String getAgentPassword() { return agentPassword; } diff --git a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java index c9df2d87..a852be81 100644 --- a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java +++ b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.data.videocall; import java.sql.Timestamp; @@ -57,6 +79,9 @@ public class VideoCallParameters { @Column(name = "IsLinkUsed") private boolean linkUsed; + @Column(name = "RecordingFileName") + private String recordingFileName; + @Column(name = "Deleted", insertable = false, updatable = true) private Boolean deleted; diff --git a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java index e41f8e80..dbf241ae 100644 --- a/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java +++ b/src/main/java/com/iemr/common/dto/dynamicForm/FieldResponseDTO.java @@ -20,7 +20,7 @@ public class FieldResponseDTO { private Integer sequence; private Boolean isEditable; private Integer stateCode; - private List options; + public List> options; private Map validation; private Map conditional; } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java index 521d5921..7e9a8f12 100644 --- a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java +++ b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java @@ -1,8 +1,29 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.mapper.videocall; import java.util.List; import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; import org.mapstruct.IterableMapping; import org.mapstruct.factory.Mappers; diff --git a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java index e7a7a3de..5d42f275 100644 --- a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java +++ b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java @@ -118,7 +118,10 @@ public class BeneficiaryModel implements Comparable { private Boolean isMarried; @Expose - private Integer doYouHavechildren; + private boolean doYouHavechildren; + + @Expose + private Integer noOfchildren; @Expose private Integer noofAlivechildren; diff --git a/src/main/java/com/iemr/common/model/user/LoginResponseModel.java b/src/main/java/com/iemr/common/model/user/LoginResponseModel.java index 10d45222..17d353e4 100644 --- a/src/main/java/com/iemr/common/model/user/LoginResponseModel.java +++ b/src/main/java/com/iemr/common/model/user/LoginResponseModel.java @@ -50,6 +50,7 @@ private MaritalStatusModel maritalstatus; private Integer statusID; private StatusModel status; + private String employeeID; private String aadhaarNo; private String pAN; private Timestamp dOB; diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java index 343198b3..39cc4e13 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java @@ -1,12 +1,35 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import lombok.Data; @Data public class UpdateCallRequest { - + private String meetingLink; - private String callStatus; - private String callDuration; + private String callStatus; + private String callDuration; private String modifiedBy; + private Boolean isLinkUsed; } diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java index f01f46f5..8843f887 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import java.sql.Timestamp; @@ -13,6 +35,7 @@ public class UpdateCallResponse { private String callDuration; private String modifiedBy; private boolean isLinkUsed; + private String recordingFileName; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Timestamp lastModified; diff --git a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java index d8a61eee..64abc044 100644 --- a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java index 4aea5698..50e84248 100644 --- a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java +++ b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java @@ -9,4 +9,10 @@ @Repository public interface FieldRepository extends JpaRepository { List findByForm_FormIdOrderBySequenceAsc(String formId); + List findByForm_FormIdAndStateCodeOrderBySequenceAsc( + String formId, + Integer stateCode + ); + + } diff --git a/src/main/java/com/iemr/common/repository/dynamic_form/FormFieldOptionRepository.java b/src/main/java/com/iemr/common/repository/dynamic_form/FormFieldOptionRepository.java new file mode 100644 index 00000000..3f258ae4 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/dynamic_form/FormFieldOptionRepository.java @@ -0,0 +1,14 @@ +package com.iemr.common.repository.dynamic_form; +import com.iemr.common.data.dynamic_from.FormFieldOption; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FormFieldOptionRepository + extends JpaRepository { + + List findByOptionKeyOrderBySortOrderAsc(String optionKey); + +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java index f6a5dcb0..139b5ee9 100644 --- a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java +++ b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java @@ -10,4 +10,5 @@ public interface TranslationRepo extends JpaRepository { Optional findByLabelKeyAndIsActive(String labelKey, boolean isActive); + } diff --git a/src/main/java/com/iemr/common/repository/users/AshaSupervisorLoginRepo.java b/src/main/java/com/iemr/common/repository/users/AshaSupervisorLoginRepo.java new file mode 100644 index 00000000..9bc07a17 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/users/AshaSupervisorLoginRepo.java @@ -0,0 +1,14 @@ +package com.iemr.common.repository.users; + +import java.util.ArrayList; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.common.data.users.AshaSupervisorMapping; + +@Repository +public interface AshaSupervisorLoginRepo extends CrudRepository { + + ArrayList findBySupervisorUserIDAndDeletedFalse(Integer supervisorUserID); +} diff --git a/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java new file mode 100644 index 00000000..07100ca4 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java @@ -0,0 +1,98 @@ +package com.iemr.common.repository.users; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.common.data.users.AshaSupervisorMapping; + +@Repository +public interface FacilityLoginRepo extends CrudRepository { + + // Get user's facility IDs from m_UserServiceRoleMapping (for ANY user) + @Query(value = "SELECT DISTINCT usrm.FacilityID " + + "FROM m_UserServiceRoleMapping usrm " + + "WHERE usrm.UserID = :userID AND usrm.Deleted = false " + + "AND usrm.FacilityID IS NOT NULL", nativeQuery = true) + List getUserFacilityIDs(@Param("userID") Integer userID); + + // Get facility details with geo names and facilityType + @Query(value = "SELECT DISTINCT f.FacilityID, f.FacilityName, " + + "f.StateID, COALESCE(s.StateName,'') AS stateName, " + + "f.DistrictID, COALESCE(d.DistrictName,'') AS districtName, " + + "f.BlockID, COALESCE(b.BlockName,'') AS blockName, " + + "COALESCE(f.RuralUrban,'') AS ruralUrban, " + + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName " + + "FROM m_facility f " + + "LEFT JOIN m_state s ON s.StateID = f.StateID " + + "LEFT JOIN m_district d ON d.DistrictID = f.DistrictID " + + "LEFT JOIN m_districtblock b ON b.BlockID = f.BlockID " + + "LEFT JOIN m_facilitytype ft ON ft.FacilityTypeID = f.FacilityTypeID " + + "WHERE f.FacilityID IN :facilityIDs AND f.Deleted = false", nativeQuery = true) + List getFacilityDetails(@Param("facilityIDs") List facilityIDs); + + // ASHA login: get supervisor details + @Query(value = "SELECT asm.supervisorUserID, u.FirstName, u.LastName, u.ContactNo, " + + "COALESCE(u.EmployeeID,'') AS employeeID " + + "FROM asha_supervisor_mapping asm " + + "JOIN m_User u ON u.UserID = asm.supervisorUserID " + + "WHERE asm.ashaUserID = :ashaUserID AND asm.deleted = false " + + "AND u.Deleted = false LIMIT 1", nativeQuery = true) + List getSupervisorForAsha(@Param("ashaUserID") Integer ashaUserID); + + @Query(value = "SELECT GenderName FROM m_gender WHERE GenderID = :genderID", nativeQuery = true) + String getGenderName(@Param("genderID") Integer genderID); + + // Villages mapped to facilities + @Query(value = "SELECT fvm.FacilityID, fvm.DistrictBranchID, dbm.VillageName " + + "FROM facility_village_mapping fvm " + + "JOIN m_DistrictBranchMapping dbm ON dbm.DistrictBranchID = fvm.DistrictBranchID " + + "WHERE fvm.FacilityID IN :facilityIDs AND fvm.Deleted = false", nativeQuery = true) + List getVillagesForFacilities(@Param("facilityIDs") List facilityIDs); + + // ASHA login: get peers at same facility (ANM, CHO, etc.) + @Query(value = "SELECT DISTINCT usrm.UserID, u.FirstName, u.LastName, r.RoleName, " + + "COALESCE(u.EmployeeID,'') AS employeeID, " + + "COALESCE(u.ContactNo,'') AS mobile " + + "FROM m_UserServiceRoleMapping usrm " + + "JOIN m_User u ON u.UserID = usrm.UserID " + + "JOIN m_Role r ON r.RoleID = usrm.RoleID " + + "WHERE usrm.FacilityID IN :facilityIDs " + + "AND usrm.UserID != :currentUserID " + + "AND usrm.Deleted = false AND u.Deleted = false", nativeQuery = true) + List getPeersAtFacility(@Param("facilityIDs") List facilityIDs, + @Param("currentUserID") Integer currentUserID); + + // Supervisor login: get mapped ASHAs with facility details + @Query(value = "SELECT DISTINCT u.UserID, u.FirstName, u.LastName, " + + "COALESCE(u.EmployeeID,'') AS employeeID, " + + "f.FacilityID, f.FacilityName, " + + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName, " + + "COALESCE(u.ContactNo,'') AS mobile " + + "FROM asha_supervisor_mapping asm " + + "JOIN m_User u ON u.UserID = asm.ashaUserID " + + "JOIN m_facility f ON f.FacilityID = asm.facilityID " + + "LEFT JOIN m_facilitytype ft ON ft.FacilityTypeID = f.FacilityTypeID " + + "WHERE asm.supervisorUserID = :supervisorUserID " + + "AND asm.deleted = false AND u.Deleted = false AND f.Deleted = false", nativeQuery = true) + List getMappedAshasBySupervisor(@Param("supervisorUserID") Integer supervisorUserID); + + // CHO/ANM login: get ASHAs at same facilities + @Query(value = "SELECT DISTINCT u.UserID, u.FirstName, u.LastName, " + + "COALESCE(u.EmployeeID,'') AS employeeID, " + + "f.FacilityID, f.FacilityName, " + + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName, " + + "COALESCE(u.ContactNo,'') AS mobile " + + "FROM m_UserServiceRoleMapping usrm " + + "JOIN m_User u ON u.UserID = usrm.UserID " + + "JOIN m_Role r ON r.RoleID = usrm.RoleID " + + "JOIN m_facility f ON f.FacilityID = usrm.FacilityID " + + "LEFT JOIN m_facilitytype ft ON ft.FacilityTypeID = f.FacilityTypeID " + + "WHERE usrm.FacilityID IN :facilityIDs " + + "AND r.RoleName = 'ASHA' " + + "AND usrm.Deleted = false AND u.Deleted = false AND f.Deleted = false", nativeQuery = true) + List getAshaListByFacilities(@Param("facilityIDs") List facilityIDs); +} diff --git a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java index cc1abccc..4a2bef76 100644 --- a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java @@ -75,7 +75,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID, @Query("SELECT u FROM User u WHERE u.userID=5718") User getAllExistingUsers(); - + User findByUserID(Long userID); @Query("SELECT u FROM User u WHERE LOWER(u.userName) = LOWER(:userName)") diff --git a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java index 251b877a..7c9cbf26 100644 --- a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java +++ b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java @@ -1,17 +1,35 @@ -package com.iemr.common.repository.videocall; +/* +* 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/. +*/ -import java.util.List; +package com.iemr.common.repository.videocall; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.data.jpa.repository.Query; import com.iemr.common.data.videocall.VideoCallParameters; -import com.iemr.common.model.videocall.VideoCallRequest; import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.annotation.Transactional; - @Repository public interface VideoCallParameterRepository extends CrudRepository { @@ -19,14 +37,28 @@ public interface VideoCallParameterRepository extends CrudRepository 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 + String labelKey = field.getFieldId(); + + Translation label = translationRepo.findByLabelKeyAndIsActive(labelKey, true) + .orElse(null); - Translation t = translationRepo.findByLabelKeyAndIsActive(labelKey, true) + Translation placeHolder = translationRepo.findByLabelKeyAndIsActive("placeholder_"+labelKey, true) .orElse(null); - String translatedLabel = field.getLabel(); // fallback + String translatedLabel = field.getLabel(); + String translatedPlaceHolder = field.getPlaceholder(); - if (t != null) { + if (label != null) { if ("hi".equalsIgnoreCase(lang)) { - translatedLabel = t.getHindiTranslation(); + translatedLabel = label.getHindiTranslation(); } else if ("as".equalsIgnoreCase(lang)) { - translatedLabel = t.getAssameseTranslation(); + translatedLabel = label.getAssameseTranslation(); } else if ("en".equalsIgnoreCase(lang)) { - translatedLabel = t.getEnglish(); + translatedLabel = label.getEnglish(); } } + if (placeHolder != null) { + if ("hi".equalsIgnoreCase(lang)) { + translatedPlaceHolder= placeHolder.getHindiTranslation(); + } else if ("as".equalsIgnoreCase(lang)) { + translatedPlaceHolder = placeHolder.getAssameseTranslation(); + } else if ("en".equalsIgnoreCase(lang)) { + translatedPlaceHolder = placeHolder.getEnglish(); + + } + } + + FieldResponseDTO dto = new FieldResponseDTO(); dto.setId(field.getId()); dto.setIsEditable(field.getIsEditable()); @@ -165,27 +187,32 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str dto.setType(field.getType()); dto.setIsRequired(field.getIsRequired()); dto.setDefaultValue(field.getDefaultValue()); - dto.setPlaceholder(field.getPlaceholder()); + dto.setPlaceholder(translatedPlaceHolder); 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); + if (field.getOptionKey() != null && !field.getOptionKey().isBlank()) { + List dbOptions = formFieldOptionRepo + .findByOptionKeyOrderBySortOrderAsc(field.getOptionKey()); + + List> translatedOptions = dbOptions.stream() + .map(opt -> { + Map map = new LinkedHashMap<>(); + map.put("id", opt.getId()); + map.put("value", opt.getValue()); + if ("hi".equalsIgnoreCase(lang)) map.put("label", opt.getLabelHi()); + else if ("as".equalsIgnoreCase(lang)) map.put("label", opt.getLabelAs()); + else map.put("label", opt.getLabelEn()); + return map; + }) + .collect(Collectors.toList()); + + dto.setOptions(translatedOptions.isEmpty() ? null : translatedOptions); + } else { dto.setOptions(null); } - - // Handle validation if (field.getValidation() != null && !field.getValidation().isBlank()) { Map validation = objectMapper.readValue(field.getValidation(), new TypeReference<>() { }); @@ -194,7 +221,6 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str dto.setValidation(null); } - // Handle conditional if (field.getConditional() != null && !field.getConditional().isBlank()) { Map conditional = objectMapper.readValue(field.getConditional(), new TypeReference<>() { }); diff --git a/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java b/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java index b4c7782e..f94b6e9a 100644 --- a/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java +++ b/src/main/java/com/iemr/common/service/feedback/FeedbackServiceImpl.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -104,6 +105,19 @@ public class FeedbackServiceImpl implements FeedbackService { private Logger logger = LoggerFactory.getLogger(FeedbackServiceImpl.class); // private ExecutorService executor = Executors.newCachedThreadPool(); + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + @Autowired private T_EpidemicOutbreakRepo t_EpidemicOutbreakRepo; @@ -736,10 +750,7 @@ private String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } diff --git a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java index 43c5c1f2..b214a9ee 100644 --- a/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java +++ b/src/main/java/com/iemr/common/service/kmfilemanager/KMFileManagerServiceImpl.java @@ -186,6 +186,7 @@ private ArrayList addKMFile(Iterable kmFileManager .replace("}", "").replace("[", "").replace("]", "").replace("|", "").replace("\\", "") .replace(":", "").replace(";", "").replace("-", "").replace("_", "").replace("+", "") .replace("=", "").replace("\"", "").replace("'", "")); + // String tempFilePath = ConfigProperties.getPropertyByName("tempFilePath"); 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/nhm_dashboard/NHM_DashboardServiceImpl.java b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java index 4448cc07..d7afd579 100644 --- a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java +++ b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.google.gson.Gson; @@ -61,6 +62,9 @@ public class NHM_DashboardServiceImpl implements NHM_DashboardService { @Autowired private DetailedCallReportRepo detailedCallReportRepo; + @Value("${cti-server-ip}") + private String serverURL; + public String pushAbandonCalls(AbandonCallSummary abandonCallSummary) throws Exception { logger.info("NHM_abandon call push API request : " + abandonCallSummary.toString()); @@ -227,7 +231,7 @@ public List callAgentSummaryReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-agent-summary-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); @@ -272,7 +276,7 @@ public List callDetailedCallReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-details-call-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); diff --git a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java index 2d8aaeb3..a4fda6f5 100644 --- a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java @@ -38,6 +38,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -70,6 +71,19 @@ public class NotificationServiceImpl implements NotificationService private EmailService emailService; + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + @Autowired public void setEmailService(EmailService emailService) { @@ -415,10 +429,7 @@ private String getFilePath(KMFileManager kmFileManager) if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } diff --git a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java index 44e1efaa..d119a85b 100644 --- a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -59,6 +60,18 @@ public class SchemeServiceImpl implements SchemeService { private KMFileManagerService kmFileManagerService; + @Value("${km-base-protocol}") + private String dmsProtocol; + + @Value("${km-base-url}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + @Autowired public void setKmFileManagerService(KMFileManagerService kmFileManagerService) { this.kmFileManagerService = kmFileManagerService; @@ -104,16 +117,13 @@ public String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } - // return fileUIDAsURI; - String message = kmFileManager.getFileUID() ; - return message; + return fileUIDAsURI; + // String message = kmFileManager.getFileUID() ; + // return message; } @Override diff --git a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java index ff6f83e9..d8587a86 100644 --- a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java +++ b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java @@ -64,7 +64,19 @@ public class CommonServiceImpl implements CommonService { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + private static final String FILE_PATH = "filePath"; /** @@ -177,10 +189,6 @@ private String getFilePath(String fileUID) { String fileUIDAsURI = null; - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; @@ -233,12 +241,13 @@ public List getSubCategoryFilesWithURL(String request) throw SubCategoryDetails subCategory = subCategoriesList.get(index); if (subCategory.getSubCatFilePath() != null && subCategory.getSubCatFilePath().length() > 0) { String subCatFilePath = subCategory.getSubCatFilePath(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); String fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + subCategory.getSubCatFilePath(); + logger.info("file url="+fileUIDAsURI); + logger.info("file path="+subCategory.getSubCatFilePath()); + logger.info("dms Path="+dmsPath); + logger.info("subcatfilePath="+subCatFilePath); + subCategory.setSubCatFilePath(fileUIDAsURI); subCategoriesList.get(index).setFileManger(kmFileManagerRepository .getKMFileLists(subCategoryDetails.getProviderServiceMapID(), subCatFilePath)); diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index efe0d16a..c49eca10 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -391,8 +391,8 @@ public SMSNotification prepareVideoCallSMS(SMSRequest request, VideoCallParamete String variable = smsParametersMap.getSmsParameterName(); String methodName = smsParametersMap.getSmsParameter().getDataName(); String variableValue = ""; - variableValue = getVideoCallData(methodName, vcParams); - smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); + // variableValue = getVideoCallData(methodName, vcParams); + // smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); if ("VideoCall".equalsIgnoreCase(smsParametersMap.getSmsParameter().getSmsParameterType())) { variableValue = getVideoCallData(methodName, vcParams); @@ -436,10 +436,15 @@ public String getVideoCallData(String methodName, VideoCallParameters videoCall) variableValue = videoCall.getCallerPhoneNumber() !=null ? videoCall.getCallerPhoneNumber().toString() : ""; break; default: - Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); - method.setAccessible(true); - Object result = method.invoke(videoCall); - variableValue = result != null ? result.toString() : ""; + try { + Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); + method.setAccessible(true); + Object result = method.invoke(videoCall); + variableValue = result != null ? result.toString() : ""; + } catch (NoSuchMethodException e) { + logger.warn("No getter found for methodName: " + methodName + " on VideoCallParameters"); + variableValue = ""; + } break; } return variableValue.trim(); @@ -678,7 +683,7 @@ private SMSNotification prepareSMS( sms.setReceivingUserID(request.getUserID()); String smsToSend = ""; BeneficiaryModel beneficiary = null; - if (request.getBeneficiaryRegID() != null) { + if (request.getBeneficiaryRegID() != null && !request.getBeneficiaryRegID().toString().isEmpty()) { List beneficiaries = searchBeneficiary.userExitsCheckWithId(request.getBeneficiaryRegID(), authToken, request.getIs1097()); if (beneficiaries.size() == 1) @@ -844,6 +849,12 @@ private String getUserData(String className, String methodName, SMSRequest reque private String getBeneficiaryData(String className, String methodName, SMSRequest request, BeneficiaryModel beneficiary) throws Exception { String variableValue = ""; + if (beneficiary == null) { + if ("phoneno".equalsIgnoreCase(methodName)) { + return request.getBenPhoneNo() != null ? request.getBenPhoneNo() : ""; + } + return ""; + } switch (methodName.toLowerCase()) { case "name": String fname = beneficiary.getFirstName() != null ? beneficiary.getFirstName() + " " : ""; @@ -875,9 +886,16 @@ private String getBeneficiaryData(String className, String methodName, SMSReques variableValue = imrName; break; default: - Class clazz = Class.forName(className); - Method method = clazz.getDeclaredMethod("get" + methodName, null); - variableValue = method.invoke(beneficiary, null).toString(); + if ("com.iemr.common.data.videocall.VideoCallParameters".equals(className)) { + VideoCallParameters vcParams = getVideoCallParameters(request.getSmsAdvice()); + if (vcParams != null) { + variableValue = getVideoCallData(methodName, vcParams); + } + } else { + Class clazz = Class.forName(className); + Method method = clazz.getDeclaredMethod("get" + methodName, null); + variableValue = method.invoke(beneficiary, null).toString(); + } break; } diff --git a/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java new file mode 100644 index 00000000..a3fc6728 --- /dev/null +++ b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java @@ -0,0 +1,225 @@ +package com.iemr.common.service.users; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONObject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.iemr.common.data.users.AshaSupervisorMapping; +import com.iemr.common.repository.users.AshaSupervisorLoginRepo; +import com.iemr.common.repository.users.FacilityLoginRepo; + +@Service +public class AshaSupervisorLoginService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @Autowired + private AshaSupervisorLoginRepo ashaSupervisorLoginRepo; + + @Autowired + private FacilityLoginRepo facilityLoginRepo; + + /** + * Common facilityData block for ALL users. + * Returns empty structure if user has no facility mapping. + */ + public JSONObject buildFacilityLoginData(Long userID, String roleName) { + JSONObject result = new JSONObject(); + result.put("location", buildEmptyLocation()); + try { + if ("ASHA Supervisor".equalsIgnoreCase(roleName)) { + enrichAshaSupervisorData(result, userID.intValue()); + } else if ("ASHA".equalsIgnoreCase(roleName)) { + enrichAshaData(result, userID.intValue()); + } else { + enrichGeneralFacilityData(result, userID.intValue()); + } + } catch (Exception e) { + logger.error("Error building facility login data for userID " + userID + ": " + e.getMessage(), e); + } + return result; + } + + private JSONObject buildEmptyLocation() { + return new JSONObject() + .put("state", "") + .put("district", "") + .put("blockOrUlb", "") + .put("locationType", ""); + } + + // ==================== ASHA ==================== + + private void enrichAshaData(JSONObject result, Integer ashaUserID) { + List facilityIDs = facilityLoginRepo.getUserFacilityIDs(ashaUserID); + if (facilityIDs == null || facilityIDs.isEmpty()) + return; + + List facilityRows = facilityLoginRepo.getFacilityDetails(facilityIDs); + if (facilityRows == null || facilityRows.isEmpty()) + return; + + populateLocation(result, facilityRows.get(0)); + + // ASHA gets a single facility (first mapped) + Object[] row = facilityRows.get(0); + JSONObject facility = new JSONObject(); + facility.put("facilityId", row[0]); + facility.put("facilityName", str(row[1])); + facility.put("facilityType", str(row[9])); + result.put("facility", facility); + + // Supervisor details + List supervisorRows = facilityLoginRepo.getSupervisorForAsha(ashaUserID); + if (supervisorRows != null && !supervisorRows.isEmpty()) { + Object[] sRow = supervisorRows.get(0); + JSONObject supervisor = new JSONObject(); + supervisor.put("userId", sRow[0]); + supervisor.put("fullName", fullName(sRow[1], sRow[2])); + supervisor.put("mobile", str(sRow[3])); + supervisor.put("employeeId", str(sRow[4]).isEmpty() ? JSONObject.NULL : str(sRow[4])); + result.put("supervisor", supervisor); + } else { + result.put("supervisor", JSONObject.NULL); + } + + // Peers at same facility + List peerRows = facilityLoginRepo.getPeersAtFacility(facilityIDs, ashaUserID); + JSONArray peers = new JSONArray(); + if (peerRows != null) { + for (Object[] pRow : peerRows) { + JSONObject peer = new JSONObject(); + peer.put("userId", pRow[0]); + peer.put("fullName", fullName(pRow[1], pRow[2])); + peer.put("role", str(pRow[3])); + peer.put("employeeId", str(pRow[4]).isEmpty() ? JSONObject.NULL : str(pRow[4])); + peer.put("mobile", str(pRow[5]).isEmpty() ? JSONObject.NULL : str(pRow[5])); + peers.put(peer); + } + } + result.put("peersAtFacility", peers); + } + + // ==================== ASHA Supervisor ==================== + + private void enrichAshaSupervisorData(JSONObject result, Integer supervisorUserID) { + ArrayList mappings = ashaSupervisorLoginRepo + .findBySupervisorUserIDAndDeletedFalse(supervisorUserID); + if (mappings == null || mappings.isEmpty()) + return; + + Set facilityIDSet = new HashSet<>(); + for (AshaSupervisorMapping m : mappings) { + if (m.getFacilityID() != null) + facilityIDSet.add(m.getFacilityID()); + } + if (facilityIDSet.isEmpty()) + return; + List facilityIDs = new ArrayList<>(facilityIDSet); + + List facilityRows = facilityLoginRepo.getFacilityDetails(facilityIDs); + if (facilityRows == null || facilityRows.isEmpty()) + return; + + populateLocation(result, facilityRows.get(0)); + + JSONArray facilitiesArray = new JSONArray(); + for (Object[] row : facilityRows) { + JSONObject facility = new JSONObject(); + facility.put("facilityId", row[0]); + facility.put("facilityName", str(row[1])); + facility.put("facilityType", str(row[9])); + facilitiesArray.put(facility); + } + result.put("facilities", facilitiesArray); + + // Fetch mapped ASHAs with facility details + List ashaRows = facilityLoginRepo.getMappedAshasBySupervisor(supervisorUserID); + JSONArray ashaList = buildAshaList(ashaRows); + result.put("ashaList", ashaList); + result.put("totalAshaCount", ashaList.length()); + } + + // ==================== General Facility User (CHO, ANM, etc.) + // ==================== + + private void enrichGeneralFacilityData(JSONObject result, Integer userID) { + List facilityIDs = facilityLoginRepo.getUserFacilityIDs(userID); + if (facilityIDs == null || facilityIDs.isEmpty()) + return; + + List facilityRows = facilityLoginRepo.getFacilityDetails(facilityIDs); + if (facilityRows == null || facilityRows.isEmpty()) + return; + + populateLocation(result, facilityRows.get(0)); + + JSONArray facilitiesArray = new JSONArray(); + for (Object[] row : facilityRows) { + JSONObject facility = new JSONObject(); + facility.put("facilityId", row[0]); + facility.put("facilityName", str(row[1])); + facility.put("facilityType", str(row[9])); + facilitiesArray.put(facility); + } + result.put("facilities", facilitiesArray); + + // ASHAs at same facilities + List ashaRows = facilityLoginRepo.getAshaListByFacilities(facilityIDs); + JSONArray ashaList = buildAshaList(ashaRows); + result.put("ashaList", ashaList); + result.put("totalAshaCount", ashaList.length()); + } + + // ==================== Shared Helpers ==================== + + private JSONArray buildAshaList(List rows) { + JSONArray list = new JSONArray(); + if (rows == null) + return list; + for (Object[] row : rows) { + JSONObject asha = new JSONObject(); + asha.put("userId", row[0]); + asha.put("fullName", fullName(row[1], row[2])); + asha.put("employeeId", str(row[3]).isEmpty() ? JSONObject.NULL : str(row[3])); + asha.put("facilityId", row[4]); + asha.put("facilityName", str(row[5])); + asha.put("facilityType", str(row[6])); + asha.put("mobile", str(row[7]).isEmpty() ? JSONObject.NULL : str(row[7])); + list.put(asha); + } + return list; + } + + private void populateLocation(JSONObject result, Object[] facilityRow) { + JSONObject location = new JSONObject(); + location.put("state", str(facilityRow[3])); + location.put("district", str(facilityRow[5])); + location.put("blockOrUlb", str(facilityRow[7])); + location.put("locationType", str(facilityRow[8])); + result.put("location", location); + } + + private String str(Object val) { + return val != null ? val.toString() : ""; + } + + private String fullName(Object first, Object last) { + return (str(first) + " " + str(last)).trim(); + } + + public String getGenderName(Integer genderID) { + if (genderID == null) + return null; + return facilityLoginRepo.getGenderName(genderID); + } +} diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index 26b7bb15..a366fa0c 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -125,6 +125,12 @@ public List getUserServiceRoleMappingForProvider(Integ List findUserIdByUserName(String userName) throws IEMRException; + boolean lockUserAccount(Long userId) throws IEMRException; + + boolean unlockUserAccount(Long userId) throws IEMRException; + + String getUserLockStatusJson(Long userId) throws IEMRException; + + boolean hasAdminPrivileges(Long userId) throws IEMRException; - } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 71d72c97..f9624f13 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -129,6 +129,8 @@ public class IEMRAdminUserServiceImpl implements IEMRAdminUserService { private SessionObject sessionObject; @Value("${failedLoginAttempt}") private String failedLoginAttempt; + @Value("${account.lock.duration.hours:24}") + private int accountLockDurationHours; // @Autowired // private ServiceRoleScreenMappingRepository ; @@ -221,79 +223,121 @@ public void setValidator(Validator validator) { } private void checkUserAccountStatus(User user) throws IEMRException { - if (user.getDeleted()) { - throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + if (user.getDeleted() != null && user.getDeleted()) { + if (user.getLockTimestamp() != null) { + long lockTimeMillis = user.getLockTimestamp().getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long lockDurationMillis = getLockDurationMillis(); + + if (currentTimeMillis - lockTimeMillis >= lockDurationMillis) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("User account auto-unlocked after {} hours lock period for user: {}", + accountLockDurationHours, user.getUserName()); + } else { + throw new IEMRException(generateLockoutErrorMessage(user.getLockTimestamp())); + } + } else { + throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + } } else if (user.getStatusID() > 2) { throw new IEMRException("Your account is not active. Please contact administrator"); } } + /** + * Common helper method for password validation and account locking logic. + * Used by both userAuthenticate() and superUserAuthenticate(). + */ + private User handlePasswordValidationAndLocking(User user, String password, int failedAttemptThreshold) + throws IEMRException, NoSuchAlgorithmException, InvalidKeySpecException { + int validatePassword = securePassword.validatePassword(password, user.getPassword()); + + switch (validatePassword) { + case 0: + // Invalid password - handle failed attempts + handleFailedLoginAttempt(user, failedAttemptThreshold); + break; + case 1: + // Valid password with old format - upgrade to new format + checkUserAccountStatus(user); + clearFailedAttemptState(user); + user.setPassword(generateUpgradedPassword(password)); + iEMRUserRepositoryCustom.save(user); + break; + case 2, 3: + // Valid password + checkUserAccountStatus(user); + clearFailedAttemptState(user); + iEMRUserRepositoryCustom.save(user); + break; + default: + // Successful validation - reset failed attempts if needed + checkUserAccountStatus(user); + resetFailedAttemptsIfNeeded(user); + break; + } + return user; + } + + private void clearFailedAttemptState(User user) { + user.setFailedAttempt(0); + user.setLockTimestamp(null); + } + + private long getLockDurationMillis() { + return (long) accountLockDurationHours * 60 * 60 * 1000; + } + + private String generateUpgradedPassword(String password) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int iterations = 1001; + char[] chars = password.toCharArray(); + byte[] salt = getSalt(); + PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + byte[] hash = skf.generateSecret(spec).getEncoded(); + return iterations + ":" + toHex(salt) + ":" + toHex(hash); + } + + private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) throws IEMRException { + int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0; + if (currentAttempts + 1 < failedAttemptThreshold) { + user.setFailedAttempt(currentAttempts + 1); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); + throw new IEMRException("Invalid username or password"); + } else { + java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); + user.setFailedAttempt(currentAttempts + 1); + user.setDeleted(true); + user.setLockTimestamp(lockTime); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", + failedAttemptThreshold); + throw new IEMRException(generateLockoutErrorMessage(lockTime)); + } + } + + private void resetFailedAttemptsIfNeeded(User user) { + if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) { + clearFailedAttemptState(user); + iEMRUserRepositoryCustom.save(user); + } + } + @Override public List userAuthenticate(String userName, String password) throws Exception { List users = iEMRUserRepositoryCustom.findByUserNameNew(userName); if (users.size() != 1) { throw new IEMRException("Invalid username or password"); } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; + int failedAttemptThreshold = getFailedAttemptThreshold(); User user = users.get(0); try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { - checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 3) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -301,16 +345,36 @@ public List userAuthenticate(String userName, String password) throws Exce return users; } - private void checkUserLoginFailedAttempt(User user) throws IEMRException { - + private int getFailedAttemptThreshold() { + if (failedLoginAttempt != null && !failedLoginAttempt.trim().isEmpty()) { + try { + return Integer.parseInt(failedLoginAttempt.trim()); + } catch (NumberFormatException e) { + logger.warn("Invalid failedLoginAttempt configuration value '{}', using default of 5", failedLoginAttempt); + } + } + return 5; } - private void updateUserLoginFailedAttempt(User user) throws IEMRException { + private String generateLockoutErrorMessage(java.sql.Timestamp lockTimestamp) { + if (lockTimestamp == null) { + return "Your account has been locked. Please contact the administrator."; + } - } + long remainingMillis = calculateRemainingLockTime(lockTimestamp); - private void resetUserLoginFailedAttempt(User user) throws IEMRException { + if (remainingMillis <= 0) { + return "Your account lock has expired. Please try logging in again."; + } + return "Your account has been locked. You can try tomorrow or connect to the administrator."; + } + + private long calculateRemainingLockTime(java.sql.Timestamp lockTimestamp) { + long lockTimeMillis = lockTimestamp.getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long unlockTimeMillis = lockTimeMillis + getLockDurationMillis(); + return unlockTimeMillis - currentTimeMillis; } /** @@ -319,67 +383,13 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException { @Override public User superUserAuthenticate(String userName, String password) throws Exception { List users = iEMRUserRepositoryCustom.findByUserName(userName); - if (users.size() != 1) { throw new IEMRException("Invalid username or password"); } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; + int failedAttemptThreshold = getFailedAttemptThreshold(); User user = users.get(0); try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { - checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -1205,12 +1215,12 @@ public User getUserById(Long userId) throws IEMRException { try { // Fetch user from custom repository by userId User user = iEMRUserRepositoryCustom.findByUserID(userId); - + // Check if user is found if (user == null) { throw new IEMRException("User not found with ID: " + userId); } - + return user; } catch (Exception e) { // Log and throw custom exception in case of errors @@ -1221,13 +1231,150 @@ public User getUserById(Long userId) throws IEMRException { @Override public List getUserIdbyUserName(String userName) { - return iEMRUserRepositoryCustom.findByUserName(userName); } - @Override + @Override public List findUserIdByUserName(String userName) { - return iEMRUserRepositoryCustom.findUserName(userName); } + + @Override + public boolean lockUserAccount(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + if (user.getDeleted() != null && user.getDeleted()) { + if (user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Activate the account before locking it."); + } + logger.info("User account is already locked for userID: {}", userId); + return false; + } + + user.setDeleted(true); + user.setFailedAttempt(getFailedAttemptThreshold()); + user.setLockTimestamp(new java.sql.Timestamp(System.currentTimeMillis())); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually locked user account for userID: {}", userId); + return true; + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error locking user account with ID: " + userId, e); + throw new IEMRException("Error locking user account: " + e.getMessage(), e); + } + } + + @Override + public boolean unlockUserAccount(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() != null) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually unlocked user account for userID: {}", userId); + return true; + } else if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Use user management to reactivate."); + } else { + logger.info("User account is not locked for userID: {}", userId); + return false; + } + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error unlocking user account with ID: " + userId, e); + throw new IEMRException("Error unlocking user account: " + e.getMessage(), e); + } + } + + @Override + public String getUserLockStatusJson(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + org.json.JSONObject status = new org.json.JSONObject(); + status.put("userId", user.getUserID()); + status.put("userName", user.getUserName()); + status.put("failedAttempts", user.getFailedAttempt() != null ? user.getFailedAttempt() : 0); + status.put("statusID", user.getStatusID()); + + boolean isDeleted = user.getDeleted() != null && user.getDeleted(); + boolean isLockedDueToFailedAttempts = isDeleted && user.getLockTimestamp() != null; + + status.put("isLocked", isDeleted); + status.put("isLockedDueToFailedAttempts", isLockedDueToFailedAttempts); + + if (isLockedDueToFailedAttempts) { + long remainingMillis = calculateRemainingLockTime(user.getLockTimestamp()); + boolean lockExpired = remainingMillis <= 0; + + status.put("lockExpired", lockExpired); + status.put("lockTimestamp", user.getLockTimestamp().toString()); + status.put("remainingTime", lockExpired ? "Lock expired - will unlock on next login" : formatRemainingTime(remainingMillis)); + if (!lockExpired) { + status.put("unlockTime", new java.sql.Timestamp(user.getLockTimestamp().getTime() + getLockDurationMillis()).toString()); + } + } else { + status.put("lockExpired", false); + status.put("lockTimestamp", org.json.JSONObject.NULL); + status.put("remainingTime", org.json.JSONObject.NULL); + } + + return status.toString(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error fetching user lock status with ID: " + userId, e); + throw new IEMRException("Error fetching user lock status: " + e.getMessage(), e); + } + } + + private String formatRemainingTime(long remainingMillis) { + long hours = remainingMillis / (60 * 60 * 1000); + long minutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000); + if (hours > 0 && minutes > 0) return String.format("%d hours %d minutes", hours, minutes); + if (hours > 0) return String.format("%d hours", hours); + return String.format("%d minutes", minutes); + } + + private static final Set ADMIN_ROLE_NAMES = Set.of("admin", "supervisor", "provideradmin"); + + @Override + public boolean hasAdminPrivileges(Long userId) throws IEMRException { + try { + List roleMappings = getUserServiceRoleMapping(userId); + if (roleMappings == null || roleMappings.isEmpty()) { + return false; + } + for (UserServiceRoleMapping mapping : roleMappings) { + Role role = mapping.getM_Role(); + if (role != null && role.getRoleName() != null) { + String roleName = role.getRoleName().trim().toLowerCase(); + if (ADMIN_ROLE_NAMES.contains(roleName)) { + return true; + } + } + } + return false; + } catch (Exception e) { + logger.error("Error checking admin privileges for userId: " + userId, e); + return false; + } + } } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java index 9322050b..81975d53 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java @@ -1,13 +1,58 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.service.videocall; -import com.iemr.common.utils.response.OutputResponse; + import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; public interface VideoCallService { - + public String generateMeetingLink() throws Exception; public String sendMeetingLink(VideoCallRequest request) throws Exception; public String updateCallStatus(UpdateCallRequest request) throws Exception; + + /** + * Resolve the short slug carried in the SMS link (the value after "?m=") + * into the full Jitsi URL with a freshly minted JWT appended. + * Called by the public redirect endpoint that the Jitsi host's nginx + * proxies "/?m=<slug>" requests to. + * + * @param slug the random slug originally generated by {@link #generateMeetingLink()} + * @return absolute URL of the form + * https://<jitsi.domain>/<jitsi.room.prefix><slug>?jwt=<token> + */ + public String resolveMeetingLink(String slug) throws Exception; + + /** + * Generate a moderator JWT URL for the agent/associate so they can join + * the Jitsi room with "End Meeting for All" privileges. + * + * @param slug the meeting slug (value after "m=" in the meeting link) + * @param agentName display name for the agent in the Jitsi UI + * @param agentEmail agent email (used for Jitsi avatar / gravatar) + * @return absolute Jitsi URL with moderator JWT appended + */ + public String generateAgentToken(String slug, String agentName, String agentEmail) throws Exception; } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 9db1a771..04518f32 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.service.videocall; import org.apache.commons.lang.RandomStringUtils; @@ -7,18 +29,12 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.sql.Timestamp; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.io.IOException; import com.iemr.common.data.videocall.VideoCallParameters; import com.iemr.common.mapper.videocall.VideoCallMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; -import com.iemr.common.utils.config.ConfigProperties; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; import org.springframework.beans.factory.annotation.Value; @@ -29,28 +45,38 @@ public class VideoCallServiceImpl implements VideoCallService { @Autowired private VideoCallParameterRepository videoCallRepository; - + @Autowired private VideoCallMapper videoCallMapper; + @Autowired + private JitsiJwtUtil jitsiJwtUtil; + private String meetingLink; private boolean isLinkSent = false; - private String consultationStatus = "Not Initiated"; - @Value("${video-call-url}") + @Value("${videocall.url}") private String jitsiLink; + @Value("${jitsi.domain}") + private String jitsiDomain; + + @Value("${jitsi.room.prefix}") + private String roomPrefix; + + @Value("${jitsi.default.user.email}") + private String defaultUserEmail; + public VideoCallServiceImpl() { - // this.jitsiLink = ConfigProperties.getPropertyByName("video-call-url"); - // logger.info("Jitsi Link fetched: " + this.jitsiLink); + // Default constructor + this.meetingLink = null; + this.isLinkSent = false; } @Override public String generateMeetingLink() { - logger.info("Jitsi Link: " + jitsiLink); meetingLink=jitsiLink+"m="+RandomStringUtils.randomAlphanumeric(8); - logger.info("Meeting link: " + meetingLink); return meetingLink; } @@ -83,54 +109,117 @@ public String sendMeetingLink(VideoCallRequest request) throws Exception { @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { - VideoCallParameters videoCall = null; + String meetingLink = callRequest.getMeetingLink(); + + // 1. Verify the row actually exists before attempting update + VideoCallParameters existing = videoCallRepository.findByMeetingLink(meetingLink); + if (existing == null) { + logger.error("[updateCallStatus] No row found in t_videocallparameter for meetingLink={}", meetingLink); + throw new Exception("No meeting found for link: " + meetingLink); + } + + // 2. Derive the two fields + boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); + String recordingFileName = buildRecordingFileName(meetingLink); + + // 3. Single atomic JPQL UPDATE — sets ALL five fields in one DB round-trip + int updateCount = videoCallRepository.updateCallStatusAndRecording( + meetingLink, + callRequest.getCallStatus(), + callRequest.getCallDuration(), + callRequest.getModifiedBy(), + linkUsed, + recordingFileName + ); + logger.info("[updateCallStatus] JPQL updateCallStatusAndRecording affected {} row(s)", updateCount); - VideoCallParameters requestEntity = videoCallMapper.updateRequestToVideoCall(callRequest); + if (updateCount == 0) { + logger.error("[updateCallStatus] Update affected 0 rows — possible meetingLink mismatch. meetingLink={}", meetingLink); + throw new Exception("Failed to update the call status — 0 rows affected"); + } - videoCall = videoCallRepository.findByMeetingLink(requestEntity.getMeetingLink()); + // 4. Re-fetch AFTER the update so the returned JSON reflects what is now in the DB + VideoCallParameters updated = videoCallRepository.findByMeetingLink(meetingLink); + + return OutputMapper.gsonWithoutExposeRestriction() + .toJson(videoCallMapper.videoCallToResponse(updated)); +} - int updateCount = videoCallRepository.updateCallStatusByMeetingLink( - requestEntity.getMeetingLink(), - requestEntity.getCallStatus(), - requestEntity.getCallDuration(), - requestEntity.getModifiedBy() - ); +/** + * Jibri records each Jitsi room into a directory named after the room, with + * the MP4 file sharing the same name — e.g. piramal-meeting-Ab3xQ9pK/piramal-meeting-Ab3xQ9pK.mp4. + * The short SMS link is "m=", so derive the room from the slug. + */ +private String buildRecordingFileName(String meetingLink) { - if (updateCount > 0) { - videoCall.setLinkUsed(true); - videoCallRepository.save(videoCall); - - // if ("Completed".equalsIgnoreCase(requestEntity.getCallStatus())) { - // saveRecordingFile(videoCall.getMeetingLink()); - // } - } else { - throw new Exception("Failed to update the call status"); + if (meetingLink == null) { + logger.warn("[buildRecordingFileName] meetingLink is null — returning null"); + return null; } - return OutputMapper.gsonWithoutExposeRestriction() - .toJson(videoCallMapper.videoCallToResponse(videoCall)); + int idx = meetingLink.lastIndexOf("m="); + if (idx < 0) { + logger.warn("[buildRecordingFileName] 'm=' marker not found in meetingLink={} — returning null", meetingLink); + return null; + } + + String slug = meetingLink.substring(idx + 2); + if (slug.isEmpty()) { + logger.warn("[buildRecordingFileName] slug is empty after 'm=' in meetingLink={} — returning null", meetingLink); + return null; + } + + String roomName = roomPrefix + slug; + String fileName = roomName + "/" + roomName + ".mp4"; + return fileName; } -private void saveRecordingFile(String meetingLink) { - try { - // Configurable Jibri recording location - String jibriOutputDir = ConfigProperties.getPropertyByName("jibri.output.path"); // e.g., /srv/jibri/recordings - String saveDir = ConfigProperties.getPropertyByName("video.recording.path"); // e.g., /srv/recordings - - File jibriDir = new File(jibriOutputDir); - File[] matchingFiles = jibriDir.listFiles((dir, name) -> name.contains(meetingLink) && name.endsWith(".mp4")); - - if (matchingFiles != null && matchingFiles.length > 0) { - File recording = matchingFiles[0]; - Path targetPath = Paths.get(saveDir, meetingLink + ".mp4"); - - Files.copy(recording.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); - logger.info("Recording file saved: " + targetPath); - } else { - logger.warn("No matching recording file found for meeting: " + meetingLink); - } - } catch (IOException e) { - logger.error("Error saving recording file: ", e); + +@Override +public String resolveMeetingLink(String slug) throws Exception { + if (slug == null || slug.isEmpty()) { + throw new IllegalArgumentException("Meeting slug is required"); } + + // SMS clients sometimes include trailing punctuation when linkifying URLs + slug = slug.replaceAll("[.,:;!?]+$", ""); + + String shortLink = jitsiLink + "m=" + slug; + VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); + + if (params == null) { + throw new Exception("No meeting found for slug: " + slug); + } + + if (params.isLinkUsed()) { + throw new Exception("This meeting link has already been used and is no longer active."); + } + + String roomName = roomPrefix + slug; + String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() + ? params.getAgentName() + : "Guest"; + + String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail, false); + String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; + + return redirectUrl; +} + +@Override +public String generateAgentToken(String slug, String agentName, String agentEmail) throws Exception { + if (slug == null || slug.isEmpty()) { + throw new IllegalArgumentException("Meeting slug is required"); + } + + // Room name is deterministic from the slug — no DB lookup needed. + // This avoids a race condition where the frontend calls this endpoint + // before /send-link has written the row. + String roomName = roomPrefix + slug; + String displayName = (agentName != null && !agentName.isEmpty()) ? agentName : "Agent"; + String email = (agentEmail != null && !agentEmail.isEmpty()) ? agentEmail : defaultUserEmail; + + String token = jitsiJwtUtil.generateRoomToken(roomName, displayName, email, true); + return "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; } } diff --git a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java index 67b642ab..397d989f 100644 --- a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java +++ b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -46,58 +47,58 @@ public class WelcomeBenificarySmsServiceImpl implements WelcomeBenificarySmsServ private String smsTemplateName = "welcome_sms"; - private String smsTemplate; + private String smsTemplate =null; - @Override + @Async public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryName, String beneficiaryId) { - final RestTemplate restTemplate = new RestTemplate(); - - Optional smsTemplateData = smsTemplateRepository.findBySmsTemplateName(smsTemplateName); - if (smsTemplateData.isPresent()) { - smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplateData.get().getSmsTemplateID()).getSmsTemplate(); - - } - - logger.info("sms template" + smsTemplate); - - - String sendSMSAPI = SMS_GATEWAY_URL; try { + String sendSMSAPI = SMS_GATEWAY_URL; - String message = smsTemplate.replace("$$BENE_NAME$$", beneficiaryName).replace("$$BEN_ID$$", beneficiaryId); - // Build payload - Map payload = new HashMap<>(); - payload.put("customerId", smsUserName); - payload.put("destinationAddress", contactNo); - payload.put("message", message); - payload.put("sourceAddress", smsSourceAddress); - payload.put("messageType", "SERVICE_IMPLICIT"); - payload.put("dltTemplateId", smsTemplateData.get().getDltTemplateId()); - payload.put("entityId", smsEntityId); - // Set headers - HttpHeaders headers = new HttpHeaders(); - String auth = smsUserName + ":" + smsPassword; - headers.add("Authorization", - "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); - - headers.setContentType(MediaType.APPLICATION_JSON); - logger.info("payload: " + payload); - HttpEntity> request = new HttpEntity<>(payload, headers); - - // Call API - ResponseEntity response = restTemplate.postForEntity(sendSMSAPI, request, String.class); - logger.info("sms-response:" + response.getBody()); - if (response.getStatusCode().value() == 200) { - return "OTP sent successfully on register mobile number"; - } else { - return "Fail"; + final RestTemplate restTemplate = new RestTemplate(); + Optional smsTemplateData = smsTemplateRepository.findBySmsTemplateName(smsTemplateName); + if (smsTemplateData.isPresent()) { + smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplateData.get().getSmsTemplateID()).getSmsTemplate(); + } + if(smsTemplate!=null){ + String message = smsTemplate.replace("$$BENE_NAME$$", beneficiaryName).replace("$$BEN_ID$$", beneficiaryId); + // Build payload + Map payload = new HashMap<>(); + payload.put("customerId", smsUserName); + payload.put("destinationAddress", contactNo); + payload.put("message", message); + payload.put("sourceAddress", smsSourceAddress); + payload.put("messageType", "SERVICE_IMPLICIT"); + payload.put("dltTemplateId", smsTemplateData.get().getDltTemplateId()); + payload.put("entityId", smsEntityId); + // Set headers + HttpHeaders headers = new HttpHeaders(); + String auth = smsUserName + ":" + smsPassword; + headers.add("Authorization", + "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); + + + headers.setContentType(MediaType.APPLICATION_JSON); + logger.info("payload: " + payload); + HttpEntity> request = new HttpEntity<>(payload, headers); + + // Call API + ResponseEntity response = restTemplate.postForEntity(sendSMSAPI, request, String.class); + logger.info("sms-response:" + response.getBody()); + if (response.getStatusCode().value() == 200) { + return "OTP sent successfully on register mobile number"; + } else { + return "Fail"; + + } } - } catch (Exception e) { + } + catch (Exception e) { return "Error sending SMS: " + e.getMessage().toString(); } + return null; } } diff --git a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java index 92d3c339..7747f6ee 100644 --- a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java +++ b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java @@ -40,12 +40,12 @@ @Configuration public class IEMRApplBeans { - @Bean - public KMService getOpenKMService() - { - KMService kmService = new OpenKMServiceImpl(); - return kmService; - } + // @Bean + // public KMService getOpenKMService() + // { + // KMService kmService = new OpenKMServiceImpl(); + // return kmService; + // } @Bean public Validator getVaidator() diff --git a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java new file mode 100644 index 00000000..229a77f1 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java @@ -0,0 +1,111 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +/** + * Mints HS256 JWTs that are accepted by the Jitsi/prosody token-auth module + * running on the video-conferencing host. This is intentionally separate from + * {@link JwtUtil} (which mints application session tokens) because the secret, + * claim set, and expiration policy are completely different. + * + * Claims produced (matches what devops configured on prosody): + * aud -> jitsi.app.id (e.g. "piramal_vc") + * iss -> jitsi.app.id (e.g. "piramal_vc") + * sub -> jitsi.sub (must always be "meet.jitsi") + * room -> the room name to admit the bearer into + * exp -> now + jitsi.token.ttl.seconds + * context.user.{name,email} -> displayed in the Jitsi UI + */ +@Component +public class JitsiJwtUtil { + + // Fallback chains let either dot-form (jitsi.app.id=...) or upper-form + // (JITSI_APP_ID=...) work in any property source, including .properties + // files which Spring does NOT relaxed-bind for @Value. + @Value("${jitsi.app.id:${JITSI_APP_ID:}}") + private String appId; + + @Value("${jitsi.app.secret:${JITSI_APP_SECRET:}}") + private String appSecret; + + @Value("${jitsi.sub:${JITSI_SUB:meet.jitsi}}") + private String sub; + + @Value("${jitsi.token.ttl.seconds:${JITSI_TOKEN_TTL_SECONDS:3600}}") + private long ttlSeconds; + + private SecretKey getSigningKey() { + if (appSecret == null || appSecret.isEmpty()) { + throw new IllegalStateException("jitsi.app.secret is not configured"); + } + return Keys.hmacShaKeyFor(appSecret.getBytes()); + } + + /** + * Build a Jitsi room JWT. + * + * @param room the exact room name the bearer will join (must match the URL path) + * @param userName display name shown in the Jitsi UI + * @param userEmail email shown in the Jitsi UI (used for gravatar etc.) + * @param isModerator when true, grants prosody moderator role — required for "End Meeting for All" + * @return signed compact JWT string + */ + public String generateRoomToken(String room, String userName, String userEmail, boolean isModerator) { + if (room == null || room.isEmpty()) { + throw new IllegalArgumentException("room is required to mint a Jitsi token"); + } + + long nowMs = System.currentTimeMillis(); + Date expiry = new Date(nowMs + (ttlSeconds * 1000L)); + + Map user = new HashMap<>(); + user.put("name", userName != null ? userName : "Guest"); + user.put("email", userEmail != null ? userEmail : ""); + user.put("moderator", isModerator); + + Map context = new HashMap<>(); + context.put("user", user); + + return Jwts.builder() + .header().add("typ", "JWT").and() + .claim("aud", appId) + .issuer(appId) + .subject(sub) + .claim("room", room) + .claim("context", context) + .expiration(expiry) + .signWith(getSigningKey(), Jwts.SIG.HS256) + .compact(); + } +} diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 381f64de..df2d1ed6 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -13,7 +13,6 @@ import com.iemr.common.data.users.User; import com.iemr.common.repository.users.IEMRUserRepositoryCustom; -import com.iemr.common.service.users.IEMRAdminUserServiceImpl; import com.iemr.common.utils.exception.IEMRException; import io.jsonwebtoken.Claims; @@ -33,9 +32,6 @@ public class JwtAuthenticationUtil { private IEMRUserRepositoryCustom iEMRUserRepositoryCustom; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - @Autowired - private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl; - public JwtAuthenticationUtil(CookieUtil cookieUtil, JwtUtil jwtUtil) { this.cookieUtil = cookieUtil; this.jwtUtil = jwtUtil; diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 364aa12d..557d5da5 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -120,6 +120,15 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("JwtUserIdValidationFilter invoked for path: " + path); + // Public video-consultation resolve endpoint: hit by SMS recipients on + // phone browsers that have no app session. Skip ALL auth — the JWT minted + // inside the handler + the unguessable slug provide access control. + if (isVideoConsultationResolvePath(path, contextPath)) { + logger.info("Video-consultation resolve path detected - skipping authentication: {}", path); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + // NEW: if this is a platform-feedback endpoint, treat it as public (skip auth) // and also ensure we don't clear any user cookies for these requests. if (isPlatformFeedbackPath(path, contextPath)) { @@ -206,6 +215,17 @@ private boolean isPlatformFeedbackPath(String path, String contextPath) { return normalized.startsWith(base + "/platform-feedback"); } + /** + * Identifies the public video-consultation resolve endpoint. + * Uses multiple matching strategies to be resilient against + * context-path mismatches between reverse-proxy and Wildfly. + */ + private boolean isVideoConsultationResolvePath(String path, String contextPath) { + if (path == null) return false; + String normalized = path.toLowerCase(); + return normalized.endsWith("/video-consultation/resolve") + || normalized.contains("/video-consultation/resolve"); + } private boolean isOriginAllowed(String origin) { if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) { @@ -253,7 +273,11 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") || path.startsWith(contextPath + "/user/refreshToken") || path.equals(contextPath + "/health") - || path.equals(contextPath + "/version"); + || path.equals(contextPath + "/version") + // Public Jitsi short-link redirect: hit by SMS recipients on phone + // browsers that have no app session. Access control is the JWT minted + // inside the redirect handler + the unguessable slug. + || path.endsWith("/video-consultation/resolve"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index 5d37a990..98ff7b7b 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -46,6 +46,27 @@ public String generateToken(String username, String userId) { return buildToken(username, userId, "access", ACCESS_EXPIRATION_TIME); } + // Mobile login: token without PII in sub + public String generateSecureToken(String userId) { + return buildSecureToken(userId, "access", ACCESS_EXPIRATION_TIME); + } + + public String generateSecureRefreshToken(String userId) { + return buildSecureToken(userId, "refresh", REFRESH_EXPIRATION_TIME); + } + + private String buildSecureToken(String userId, String tokenType, long expiration) { + return Jwts.builder() + .subject(userId) + .claim("userId", userId) + .claim("token_type", tokenType) + .id(UUID.randomUUID().toString()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + /** * Generate a refresh token. * @@ -163,6 +184,10 @@ public long getRefreshTokenExpiration() { return REFRESH_EXPIRATION_TIME; } + public long getAccessTokenExpiration() { + return ACCESS_EXPIRATION_TIME; + } + /** * Extract user ID from JWT token in the request (checks header and cookie) * @param request the HTTP request diff --git a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java index 59b69b82..43a49364 100644 --- a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java +++ b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java @@ -144,11 +144,21 @@ public static String getPropertyByName(String propertyName) String result = null; try { - if (properties == null) + if (environment != null) { - initalizeProperties(); + result = environment.getProperty(propertyName); + } + if (result == null) + { + if (properties == null) + { + initalizeProperties(); + } + result = properties.getProperty(propertyName).trim(); + } else + { + result = result.trim(); } - result = properties.getProperty(propertyName).trim(); } catch (Exception e) { logger.error(propertyName + " retrival failed.", e); diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index b4aaad60..757e59d9 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -125,6 +125,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons case "validateSecurityQuestionAndAnswer": case "logOutUserFromConcurrentSession": case "refreshToken": + case "resolve": break; case "error": status = false; diff --git a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java index 2be04cfc..4a6e68ec 100644 --- a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java +++ b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java @@ -46,44 +46,47 @@ import com.openkm.sdk4j.exception.VirusDetectedException; import com.openkm.sdk4j.exception.WebserviceException; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; +import jakarta.annotation.PostConstruct; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service public class OpenKMServiceImpl implements KMService { - // private ConfigProperties configProperties; - // - // @Autowired - // public void setConfigProperties(ConfigProperties configProperties) - // { - // this.configProperties = configProperties; - // } + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private static String url; - private static String username; - private static String password; - private static String kmRootPath; - private static String guestUser; - private static String guestPassword; + @Value("${km-base-url}") + private String url; + + @Value("${km-username}") + private String username; + + @Value("${km-password}") + private String password; + + @Value("${km-root-path}") + private String kmRootPath; + + @Value("${km-guest-user}") + private String guestUser; + + @Value("${km-guest-password}") + private String guestPassword; public OpenKMServiceImpl() { } - private static OKMWebservices connector = null; + private OKMWebservices connector; + @PostConstruct public void init() { - if (connector == null) { - url = ConfigProperties.getPropertyByName("km-base-url"); - username = ConfigProperties.getPropertyByName("km-username"); - password = ConfigProperties.getPropertyByName("km-password"); - kmRootPath = ConfigProperties.getPropertyByName("km-root-path"); - guestUser = ConfigProperties.getPropertyByName("km-guest-user"); - guestPassword = ConfigProperties.getPropertyByName("km-guest-password"); - connector = OpenKMConnector.initialize(url, username, password); + logger.info("KM URL=",url); + connector = OpenKMConnector.initialize(url, username, password); - } - } + } @Override public String getDocumentRoot() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fef088ff..4a2de342 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -71,17 +71,20 @@ get-agent-summary-report-URL=http://CTI_SERVER/apps/customize_apps/piramil_repor ## agent summary report API get-details-call-report-URL=http://CTI_SERVER/apps/customize_apps/piramil_reports.php?report_type=acd&format=json&end_date=END_DATE&start_date=START_DATE +##1097 abandoned call report API +get-disposition-count-URL=http://CTI_SERVER/apps/CZUtilAPI.php + #============================================================================ # Configure Main Scheduler Properties #============================================================================ - + org.quartz.scheduler.instanceId = AUTO org.quartz.scheduler.makeSchedulerThreadDaemon = true - + #============================================================================ # Configure ThreadPool #============================================================================ - + org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.makeThreadsDaemons = true org.quartz.threadPool.threadCount: 20 @@ -169,6 +172,9 @@ quality-Audit-PageSize=5 ## max no of failed login attempt failedLoginAttempt=5 +## account lock duration in hours (24 hours = 1 day for auto-unlock) +account.lock.duration.hours=24 + #Jwt Token configuration jwt.access.expiration=28800000 jwt.refresh.expiration=604800000 @@ -179,14 +185,14 @@ jwt.refresh.expiration=604800000 ## KM Configuration -km-base-protocol=http -km-username=okmAdmin -km-password=admin -km-base-url=http://localhost:8084/OpenKM -km-base-path=localhost:8084/OpenKM -km-root-path=/okm:personal/users/ -km-guest-user=guest -km-guest-password=guest +# km-base-protocol=http +# km-username=okmAdmin +# km-password=admin +# km-base-url=http://localhost:8084/OpenKM +# km-base-path=localhost:8084/OpenKM +# km-root-path=/okm:personal/users/ +# km-guest-user=guest +# km-guest-password=guest # CTI Config cti-server-ip=10.208.122.99 diff --git a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java index b3b82380..705beffa 100644 --- a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java +++ b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java @@ -36,7 +36,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -195,4 +197,39 @@ void shouldReturnOkWithErrorInBody_whenUpdateCallStatusServiceFails() throws Exc verify(videoCallService, times(1)).updateCallStatus(any(UpdateCallRequest.class)); } + + // Tests for resolveMeetingLink() - public redirect endpoint hit by SMS recipients + @Test + void shouldReturn302WithJitsiUrl_whenResolveMeetingLinkSucceeds() throws Exception { + String fullJitsiUrl = "https://vc.piramalswasthya.org/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN"; + when(videoCallService.resolveMeetingLink(eq("Ab3xQ9pK"))).thenReturn(fullJitsiUrl); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "Ab3xQ9pK")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", fullJitsiUrl)); + + verify(videoCallService, times(1)).resolveMeetingLink("Ab3xQ9pK"); + } + + @Test + void shouldReturn400_whenResolveMeetingLinkSlugIsInvalid() throws Exception { + when(videoCallService.resolveMeetingLink(eq(""))) + .thenThrow(new IllegalArgumentException("Meeting slug is required")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "")) + .andExpect(status().isBadRequest()); + + verify(videoCallService, times(1)).resolveMeetingLink(""); + } + + @Test + void shouldReturn404_whenResolveMeetingLinkSlugUnknown() throws Exception { + when(videoCallService.resolveMeetingLink(eq("missing"))) + .thenThrow(new Exception("No meeting found for slug: missing")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "missing")) + .andExpect(status().isNotFound()); + + verify(videoCallService, times(1)).resolveMeetingLink("missing"); + } } \ No newline at end of file diff --git a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java index baed9029..f8ef8add 100644 --- a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java +++ b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java @@ -26,6 +26,7 @@ import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.config.ConfigProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,10 +60,15 @@ public class VideoCallServiceImplTest { UpdateCallRequest updateCallRequest; @Mock VideoCallParameters videoCallParameters; + @Mock + JitsiJwtUtil jitsiJwtUtil; @BeforeEach public void setup() throws Exception { ReflectionTestUtils.setField(service, "jitsiLink", "https://meet.jit.si/"); + ReflectionTestUtils.setField(service, "jitsiDomain", "meet.jit.si"); + ReflectionTestUtils.setField(service, "roomPrefix", "piramal-meeting-"); + ReflectionTestUtils.setField(service, "defaultUserEmail", "admin@piramalswasthya.org"); } @Test @@ -175,6 +181,71 @@ public void testSaveRecordingFile_noMatchingFile() throws Exception { } } + @Test + public void testResolveMeetingLink_success() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn("Dr. Asha"); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Dr. Asha"), + eq("admin@piramalswasthya.org"), + eq(false))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertEquals( + "https://meet.jit.si/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN", + result); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Dr. Asha", "admin@piramalswasthya.org", false); + } + + @Test + public void testResolveMeetingLink_emptySlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink("")); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_nullSlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink(null)); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_notFound() { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=missing")) + .thenReturn(null); + + Exception ex = assertThrows( + Exception.class, + () -> service.resolveMeetingLink("missing")); + assertTrue(ex.getMessage().contains("No meeting found")); + } + + @Test + public void testResolveMeetingLink_fallbackUserNameWhenAgentMissing() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn(null); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Guest"), + eq("admin@piramalswasthya.org"), + eq(false))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertTrue(result.endsWith("?jwt=FAKE.JWT.TOKEN")); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Guest", "admin@piramalswasthya.org", false); + } + @Test public void testSaveRecordingFile_ioException() throws Exception { try (MockedStatic configMock = mockStatic(ConfigProperties.class); diff --git a/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java new file mode 100644 index 00000000..3d89480b --- /dev/null +++ b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java @@ -0,0 +1,144 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +class JitsiJwtUtilTest { + + // Same secret format as the one devops gave us (HS256, length must be >=32 bytes for Keys.hmacShaKeyFor) + private static final String APP_ID = "piramal_vc"; + private static final String APP_SECRET = "5b9883418be6f228ffe3ceaa74dd3d3b91737733a4a85c5e82fc584ad449850b"; + private static final String SUB = "meet.jitsi"; + + private JitsiJwtUtil util; + + @BeforeEach + void setUp() { + util = new JitsiJwtUtil(); + ReflectionTestUtils.setField(util, "appId", APP_ID); + ReflectionTestUtils.setField(util, "appSecret", APP_SECRET); + ReflectionTestUtils.setField(util, "sub", SUB); + ReflectionTestUtils.setField(util, "ttlSeconds", 3600L); + } + + @Test + void generateRoomToken_producesAllRequiredClaims() { + String token = util.generateRoomToken("piramal-meeting-Ab3xQ9pK", "Dr. Asha", "asha@piramalswasthya.org", false); + + assertNotNull(token); + assertTrue(token.split("\\.").length == 3, "JWT should have 3 dot-separated parts"); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + assertEquals(APP_ID, claims.getIssuer()); + assertTrue(claims.getAudience().contains(APP_ID)); + assertEquals(SUB, claims.getSubject()); + assertEquals("piramal-meeting-Ab3xQ9pK", claims.get("room", String.class)); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + assertNotNull(context); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertNotNull(user); + assertEquals("Dr. Asha", user.get("name")); + assertEquals("asha@piramalswasthya.org", user.get("email")); + assertEquals(false, user.get("moderator")); + + Date exp = claims.getExpiration(); + assertNotNull(exp); + assertTrue(exp.after(new Date()), "exp should be in the future"); + } + + @Test + void generateRoomToken_moderatorClaimTrueForAgent() { + String token = util.generateRoomToken("piramal-meeting-Ab3xQ9pK", "Dr. Asha", "asha@piramalswasthya.org", true); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertEquals(true, user.get("moderator")); + } + + @Test + void generateRoomToken_fallsBackToGuestWhenUserNameNull() { + String token = util.generateRoomToken("piramal-meeting-xyz", null, null, false); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertEquals("Guest", user.get("name")); + assertEquals("", user.get("email")); + } + + @Test + void generateRoomToken_rejectsEmptyRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken("", "Dr. Asha", "asha@piramalswasthya.org", false)); + } + + @Test + void generateRoomToken_rejectsNullRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken(null, "Dr. Asha", "asha@piramalswasthya.org", false)); + } + + @Test + void generateRoomToken_failsWhenAppSecretMissing() { + ReflectionTestUtils.setField(util, "appSecret", ""); + assertThrows(IllegalStateException.class, + () -> util.generateRoomToken("piramal-meeting-xyz", "Dr. Asha", "asha@piramalswasthya.org", false)); + } +} From b4ecdc5c916a50ed7f7e4e3b3027c27a58addb0b Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Fri, 22 May 2026 14:38:58 +0530 Subject: [PATCH 4/5] Merge 3.8.1 to Main (#422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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> * add facilityData with employeeId in login response (#385) * 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> * feat: added the facilty in resonse * fix: login response change * feat: added the facilty in resonse * fix: logoin response * fix: login response --------- 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: Mithun James Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: vanitha1822 Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) * fix: property values are not fetching after deploying in linux server * feature for hindi translation for CG * 1097 Abandon call real time data (#365) * fix: integrate API for get disposition count * fix: respnse structure * fix: coderabbit comments * fix: fetch from .env (#369) * add otp_consent template key * fix welcome sms code * fix welcome sms code * fix: login response asha list misisng * fix: occupationID and educationID not saved during beneficiary update setDemographicDetails() was overwriting occupationName (already set correctly by the mapper from occupationID) with null when no occupation name string was present in the payload. Added null guards so the mapper's resolved name is preserved, and explicitly set occupationId/educationId from i_bendemographics to ensure the IDs always reach Identity-API. Co-Authored-By: Claude Sonnet 4.6 * Revert "fix: property values are not fetching after deploying in linux server" This reverts commit eb917b2eb4901874502a598bc46d751010304524. * fix: concurrent session logout not invalidating JWT in first system logOutUserFromConcurrentSession only cleaned up old-style Redis session keys but never added the displaced user's JWT to the denylist. Because JwtUserIdValidationFilter validates solely via JWT signature and the denylist, System 1's token remained valid and all APIs returned 200 after System 2 forced a concurrent login. Fix: store a username→JTI mapping in Redis at login time; during concurrent-session logout, look up the JTI and add it to the denylist and evict the user_ cache so the next request from System 1 is rejected with 401 and the frontend shows the session-expiry message. Co-Authored-By: Claude Sonnet 4.6 * fix: concurrent session logout not invalidating JWT on first system logOutUserFromConcurrentSession only cleaned up old-style Redis session keys but never added the displaced user's JWT to the denylist. Because JwtUserIdValidationFilter validates solely via JWT signature and the denylist, System 1's token remained valid and all APIs returned 200 after System 2 forced a concurrent login. The root serialization bug: redisTemplate value serializer is Jackson2JsonRedisSerializer, so storing a plain String JTI caused a deserialization failure on retrieval. Fixed by using the existing StringRedisTemplate bean for the jti: key operations. Fix: - Store username->JTI mapping via StringRedisTemplate at login (both userAuthenticate and superUserAuthenticate) - On concurrent-session logout, retrieve the JTI, add it to the denylist, evict user_ from User cache, and clean up jti: key - Add getAccessTokenExpiration() to JwtUtil to supply the TTL Co-Authored-By: Claude Sonnet 4.6 * fix(security): remove PII from JWT token for mobile logins (#413) Add generateSecureToken/generateSecureRefreshToken methods that use userId as sub instead of username. Mobile logins (okhttp User-Agent) use the secure token — web logins remain unchanged for backward compatibility. Other services will be migrated one by one. Co-authored-by: Claude Sonnet 4.6 * feat: add mobile to peersAtFacility and ashaList in facilityData login response (#417) Co-authored-by: Claude Sonnet 4.6 * Merge Releases 3.7.0 and 3.8.1 (#416) * implement translation in dynamic form * 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> * Restrict user when account is locked * 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: merge 3.7.0 to main * Video Consultation Functionality (#380) * Update application.properties * add column in create BeneficiaryModel * 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 * update language * update language * Downgrade version from 3.6.1 to 3.6.0 * 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 * Remove empty line in application.properties * fix:signature check for mmu * Update application.properties * Update application.properties * fix: retrive any user without deleted * implement state wise hide un hide form fields * implement state wise hide un hide form fields * implement state wise hide un hide form fields * enhance welcome sms code * fix hide unhide form issue * docs: add DeepWiki badge and documentation link * Add DeepWiki badge to README Added DeepWiki badge to README for better visibility. * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * fix hide unhide form issue * 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 * 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 * 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 * 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 * 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> * fix: video consultation functionality * fix: pom version update * fix: add cti-server-ip * fix: comment unwanted code * fix: update videocall url property * fix: update cti-server-ip * docs: add CLAUDE.md for Claude Code guidance Co-Authored-By: Claude Opus 4.6 (1M context) * fix: KM issue * fix: KM issue * fix: remove unwanted imports * fix: conflicts * fix: update the temp path * Fix the OpenKM Issue (#389) * fix: remove km in application.properties * fix: update all the properties to fetch from env * fix: update path * fix: KM issue * fix: get file from km * fix: build issue * fix: build issue * fix: remove unwanted imports * fix: build issue * fix: remove commented line * Enable KM configuration in common_example.properties Uncomment KM configuration properties for OpenKM. * Fix ConfigProperties to resolve env variable placeholders via Spring Environment (#390) Co-authored-by: Claude Opus 4.6 (1M context) * fix: update sms issue * fix: build issue * fix: update condition * fix: edit ben issue * fix: phone number issue for sms * fix: update the url with jwt token * fix: jitsi authorization issue * fix: skip auth * fix: hash key updation * fix: jwt type in header for authorization * fix: update file path * fix: vc recording path updation * fix: update video call recording functionality * fix: remove unwanted codes * fix: coderabbit comments --------- Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Saurav Mishra Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: vishwab1 Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: DurgaPrasad-54 Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Vaishnav Bhosale Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH * Fix the Build Issue (#397) * fix: build issue * fix: build issue * fix: merge with main * 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 the End Consultation Call for VC (#407) * fix: end the consultation on clicking the end meeting button * fix: end call * fix: the build issue * fix: the issue in agent-token * fix: remove slug (#414) * fix: build issue --------- Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: vishwab1 Co-authored-by: DurgaPrasad-54 Co-authored-by: Vaishnav Bhosale Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH * fix: aam-2313 phone number leading with zero - removed zero (#418) --------- Co-authored-by: Sachin Kadam <152252767+sac2kadam@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: vishwab1 Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Sonnet 4.6 Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH Co-authored-by: Saurav Mishra Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: DurgaPrasad-54 Co-authored-by: Vaishnav Bhosale --- .../service/beneficiary/IdentityBeneficiaryServiceImpl.java | 3 +++ .../com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 e88edc5b..d940f8a5 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java @@ -287,6 +287,9 @@ private String cleanPhoneNumber(String phoneNumber) { // 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); + } else if (cleaned.startsWith("0") && cleaned.length() == 11) { + // Handle case where number starts with 0 and is 11 digits long + cleaned = cleaned.substring(1); } return cleaned.trim(); diff --git a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java index 4a6e68ec..5b32af7b 100644 --- a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java +++ b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java @@ -48,13 +48,14 @@ import jakarta.annotation.PostConstruct; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; - @Service +// @Primary public class OpenKMServiceImpl implements KMService { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); From be92d7c5cbbd993b569213194eb691e803ef91d0 Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Tue, 16 Jun 2026 17:14:44 +0530 Subject: [PATCH 5/5] fix: extented the token expiry timing --- src/main/java/com/iemr/common/utils/CookieUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/CookieUtil.java b/src/main/java/com/iemr/common/utils/CookieUtil.java index 92c071c5..1562e3c4 100644 --- a/src/main/java/com/iemr/common/utils/CookieUtil.java +++ b/src/main/java/com/iemr/common/utils/CookieUtil.java @@ -35,8 +35,8 @@ public void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, H // Make the cookie HttpOnly to prevent JavaScript access for security cookie.setHttpOnly(true); - // Set the Max-Age (expiry time) in seconds (8 hours) - cookie.setMaxAge(60 * 60 * 8); // 8 hours expiration + // Set the Max-Age (expiry time) in seconds (10 hours) + cookie.setMaxAge(60 * 60 * 10); // 10 hours expiration // Set the path to "/" so the cookie is available across the entire application cookie.setPath("/");