From 8251770985a70723e58f3e6ea4f6cdbb903cb609 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Tue, 19 May 2026 10:22:53 +0530 Subject: [PATCH 1/7] feat: add mobile to peersAtFacility and ashaList in facilityData login response (#417) Co-authored-by: Claude Sonnet 4.6 --- .../iemr/common/repository/users/FacilityLoginRepo.java | 9 ++++++--- .../common/service/users/AshaSupervisorLoginService.java | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java index 5623448d..07100ca4 100644 --- a/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java +++ b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java @@ -55,7 +55,8 @@ public interface FacilityLoginRepo extends CrudRepository getPeersAtFacility(@Param("facilityIDs") List facilityID @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(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 " @@ -82,7 +84,8 @@ List getPeersAtFacility(@Param("facilityIDs") List facilityID @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(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 " diff --git a/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java index d4ce1420..a3fc6728 100644 --- a/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java +++ b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java @@ -102,6 +102,7 @@ private void enrichAshaData(JSONObject result, Integer ashaUserID) { 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); } } @@ -193,6 +194,7 @@ private JSONArray buildAshaList(List rows) { 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; From b4b15be18f0d74f3418d79161d3f807d1f40738b Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Tue, 19 May 2026 10:25:37 +0530 Subject: [PATCH 2/7] 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 --- 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 | 3 + .../dynamicForm/DynamicFormController.java | 2 + .../controller/users/IEMRAdminController.java | 10 + .../controller/version/VersionController.java | 2 + .../videocall/VideoCallController.java | 129 +++++++++++- .../common/data/translation/Translation.java | 1 + .../data/videocall/VideoCallParameters.java | 25 +++ .../mapper/videocall/VideoCallMapper.java | 23 +- .../model/beneficiary/BeneficiaryModel.java | 5 +- .../model/videocall/UpdateCallRequest.java | 29 ++- .../model/videocall/UpdateCallResponse.java | 23 ++ .../model/videocall/VideoCallRequest.java | 22 ++ .../dynamic_form/FieldRepository.java | 6 + .../translation/TranslationRepo.java | 1 + .../VideoCallParameterRepository.java | 52 ++++- .../IEMRSearchUserServiceImpl.java | 2 + .../IdentityBeneficiaryServiceImpl.java | 11 + .../RegisterBenificiaryServiceImpl.java | 10 +- .../common/service/cti/CTIServiceImpl.java | 62 ++---- .../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 +++- .../service/videocall/VideoCallService.java | 49 ++++- .../videocall/VideoCallServiceImpl.java | 197 +++++++++++++----- .../WelcomeBenificarySmsServiceImpl.java | 5 +- .../com/iemr/common/utils/IEMRApplBeans.java | 12 +- .../com/iemr/common/utils/JitsiJwtUtil.java | 111 ++++++++++ .../common/utils/JwtAuthenticationUtil.java | 4 - .../utils/JwtUserIdValidationFilter.java | 26 ++- .../common/utils/config/ConfigProperties.java | 16 +- .../utils/http/HTTPRequestInterceptor.java | 1 + .../utils/km/openkm/OpenKMServiceImpl.java | 59 +++--- src/main/resources/application.properties | 22 +- .../videocall/VideoCallControllerTest.java | 37 ++++ .../videocall/VideoCallServiceImplTest.java | 71 +++++++ .../iemr/common/utils/JitsiJwtUtilTest.java | 144 +++++++++++++ 46 files changed, 1128 insertions(+), 226 deletions(-) create mode 100644 src/main/java/com/iemr/common/utils/JitsiJwtUtil.java create mode 100644 src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index f2b774a3..f39faf2f 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 a5c633e4..e3851b54 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 e3b5c031..7ec9c410 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 @@ -200,9 +200,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 @@ -233,3 +233,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 0fb758b0..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; 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 845fe890..349c3b1e 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -337,6 +337,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."); 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/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/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/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/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/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/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 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/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 1cbb1d8c..397d989f 100644 --- a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java +++ b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java @@ -77,6 +77,7 @@ public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryNa String auth = smsUserName + ":" + smsPassword; headers.add("Authorization", "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); + headers.setContentType(MediaType.APPLICATION_JSON); logger.info("payload: " + payload); @@ -93,8 +94,8 @@ public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryNa } } - - } 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/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..68947e1a 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,49 @@ 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 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 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 4ce65cad..e827b0fc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -77,14 +77,14 @@ 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 @@ -182,14 +182,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 3437e7455900c43cf509ede188bfca6563bf9929 Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Thu, 21 May 2026 12:58:12 +0530 Subject: [PATCH 3/7] fix: aam-2313 phone number leading with zero - removed zero (#418) --- .../service/beneficiary/IdentityBeneficiaryServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) 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(); From 3e0bbb798eb8b1894858ab7caa438976866fea9b Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Fri, 22 May 2026 19:05:21 +0530 Subject: [PATCH 4/7] Sn/3.8.1 (#423) * fix: aam-2313 phone number leading with zero - removed zero * fix: allow concurrent sessions for admin, superadmin, and supervisor roles Co-Authored-By: Claude Sonnet 4.6 * fix: added admin and superadmin for the condition --------- Co-authored-by: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 349c3b1e..5ac134cd 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,6 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -180,11 +181,22 @@ public String userAuthenticate( if (m_User.getUserName() != null && (m_User.getDoLogout() == null || !m_User.getDoLogout()) && (m_User.getWithCredentials() != null && m_User.getWithCredentials())) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + String userRole = ""; + if (mUser.size() == 1 && mUser.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUser.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); @@ -412,16 +424,28 @@ public String logOutUserFromConcurrentSession( deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); - // Denylist the active JWT so System 1's requests are immediately rejected - String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); - if (jtiData != null) { - String[] parts = jtiData.split("\\|", 2); - tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); - if (parts.length > 1) { - redisTemplate.delete("user_" + parts[1]); + String userRole = ""; + if (mUsers.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUsers.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + // Denylist the active JWT so the first system's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + redisTemplate.delete("jti:" + usernameKey); } - stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); From cbb18177013dedbed09d83cddcb0414d2aab162d Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:13:52 +0530 Subject: [PATCH 5/7] fix: use stringRedisTemplate for jti: key read/delete in concurrent session logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redisTemplate has Jackson2JsonRedisSerializer as value serializer, so reading the plain-string jti: value caused a deserialization failure (statusCode 5000). jti: keys are written via stringRedisTemplate at login, so reads and deletes must also use stringRedisTemplate — restoring the behaviour from commit 80fa0e5e that was accidentally reverted in #423. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/common/controller/users/IEMRAdminController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 5ac134cd..f9942b5e 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -436,7 +436,7 @@ public String logOutUserFromConcurrentSession( if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { // Denylist the active JWT so the first system's requests are immediately rejected String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); if (jtiData != null) { String[] parts = jtiData.split("\\|", 2); String jti = parts[0]; @@ -444,7 +444,7 @@ public String logOutUserFromConcurrentSession( if (parts.length > 1) { redisTemplate.delete("user_" + parts[1]); } - redisTemplate.delete("jti:" + usernameKey); + stringRedisTemplate.delete("jti:" + usernameKey); } } From 8b548d383988d62fa2354b08f95124fdda643387 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:48:34 +0530 Subject: [PATCH 6/7] fix: correct exempt roles and allow superadmin concurrent sessions - Fix role name from 'admin' to 'provideradmin' to match actual DB value - Add concurrent session exemption to superUserAuthenticate so SuperAdmin can log in from multiple tabs without being blocked Co-Authored-By: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index f9942b5e..f23c1739 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,7 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); - private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -560,11 +560,13 @@ public String superUserAuthenticate( String refreshToken = null; boolean isMobile = false; if (m_User.getUserName() != null && (m_User.getDoLogout() == null || m_User.getDoLogout() == false)) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(m_User.getUserName().trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); From 39c9d00316bf0cde73084a6fd235f5acae9104a6 Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Wed, 27 May 2026 15:31:05 +0530 Subject: [PATCH 7/7] fix: prevent JWT cookie accumulation in outgoing HTTP headers Shared HttpHeaders field in HttpUtils was mutated via add() on every request, causing Cookie header to grow unbounded across calls. CTI server rejected requests once the header exceeded its size limit. post() and getV1() now create fresh HttpHeaders per request, consistent with the existing get() pattern. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/common/utils/http/HttpUtils.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/http/HttpUtils.java b/src/main/java/com/iemr/common/utils/http/HttpUtils.java index 4f49e662..6b18bb58 100644 --- a/src/main/java/com/iemr/common/utils/http/HttpUtils.java +++ b/src/main/java/com/iemr/common/utils/http/HttpUtils.java @@ -77,8 +77,10 @@ public String get(String uri) { } public ResponseEntity getV1(String uri) throws URISyntaxException, MalformedURLException { - RestTemplateUtil.getJwttokenFromHeaders(headers); - HttpEntity requestEntity = new HttpEntity("", headers); + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add("Content-Type", "application/json"); + RestTemplateUtil.getJwttokenFromHeaders(requestHeaders); + HttpEntity requestEntity = new HttpEntity("", requestHeaders); ResponseEntity responseEntity = rest.exchange(uri, HttpMethod.GET, requestEntity, String.class); return responseEntity; } @@ -104,8 +106,10 @@ public String get(String uri, HashMap header) { public String post(String uri, String json) { String body; - RestTemplateUtil.getJwttokenFromHeaders(headers); - HttpEntity requestEntity = new HttpEntity(json, headers); + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add("Content-Type", "application/json"); + RestTemplateUtil.getJwttokenFromHeaders(requestHeaders); + HttpEntity requestEntity = new HttpEntity(json, requestHeaders); ResponseEntity responseEntity = rest.exchange(uri, HttpMethod.POST, requestEntity, String.class); setStatus((HttpStatus) responseEntity.getStatusCode()); body = responseEntity.getBody();