diff --git a/pom.xml b/pom.xml index b4e8c0d9..ffe74404 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.8.2 war Common-API diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 064090b3..aca7345a 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -218,4 +218,4 @@ otp.ratelimit.enabled=${OTP_RATELIMIT_ENABLED} otp.ratelimit.minute-limit=${OTP_RATELIMIT_MINUTE_LIMIT} otp.ratelimit.hour-limit=${OTP_RATELIMIT_HOUR_LIMIT} otp.ratelimit.day-limit=${OTP_RATELIMIT_DAY_LIMIT} -generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL} +generateBeneficiaryIDs-api-url=${GEN_BENEFICIARY_IDS_API_URL} diff --git a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java index ebb697ec..08d9a518 100644 --- a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java +++ b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java @@ -21,16 +21,19 @@ public class FirebaseMessagingConfig { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - @Value("${firebase.enabled:false}") + @Value("${firebase.enabled}") private boolean firebaseEnabled; - @Value("${firebase.credential-file:}") + @Value("${firebase.credential-file}") private String firebaseCredentialFile; @Bean @ConditionalOnProperty(name = "firebase.enabled", havingValue = "true") public FirebaseMessaging firebaseMessaging() throws IOException { + logger.info("===== Initializing Firebase ====="); + logger.info("firebaseEnabled={}", firebaseEnabled); + logger.info("firebaseCredentialFile={}", firebaseCredentialFile); if (!firebaseEnabled) { logger.error("⚠️ Firebase disabled by config"); return null; @@ -42,9 +45,11 @@ public FirebaseMessaging firebaseMessaging() throws IOException { return null; // don't throw, app will still start } - GoogleCredentials credentials = GoogleCredentials.fromStream( - new ClassPathResource(firebaseCredentialFile).getInputStream() - ); + GoogleCredentials credentials = + GoogleCredentials.fromStream( + new FileInputStream(firebaseCredentialFile) + ); + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); diff --git a/src/main/java/com/iemr/common/controller/connect/ConnectController.java b/src/main/java/com/iemr/common/controller/connect/ConnectController.java new file mode 100644 index 00000000..7ecf45ba --- /dev/null +++ b/src/main/java/com/iemr/common/controller/connect/ConnectController.java @@ -0,0 +1,57 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.controller.connect; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.common.utils.NetworkUtil; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Exposes the server's LAN address so a mobile device on the same wifi + * network can connect to this API. + */ +@RestController +@RequestMapping(value = "/public/connect") +public class ConnectController { + + @Value("${server.port:8080}") + private int serverPort; + + @Operation(summary = "Get the server's LAN IP address and port") + @GetMapping(value = "/info", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConnectInfo() { + Map response = new LinkedHashMap<>(); + response.put("ip", NetworkUtil.getLanIPAddress()); + response.put("port", serverPort); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java index 3bb10cfc..1cb2753d 100644 --- a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java +++ b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java @@ -28,9 +28,12 @@ import com.iemr.common.model.notification.UserToken; import com.iemr.common.service.firebaseNotification.FirebaseNotificationService; import com.iemr.common.utils.exception.IEMRException; +import com.iemr.common.utils.response.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -41,17 +44,60 @@ public class FirebaseNotificationController { @Autowired FirebaseNotificationService firebaseNotificationService; - @RequestMapping(value = "sendNotification",method = RequestMethod.POST,headers = "Authorization") - public String sendNotificationByToken(@RequestBody NotificationMessage notificationMessage){ - return firebaseNotificationService.sendNotification(notificationMessage); + + @PostMapping("/sendNotification") + public ResponseEntity sendNotificationByToken(@RequestBody NotificationMessage notificationMessage) { + + logger.info("Received notification request. Token={}, Title={}, ReceiverId={}, Type={}", + notificationMessage.getToken(), + notificationMessage.getTitle(), + notificationMessage.getData() != null ? notificationMessage.getData().get("receiver_user_id") : null, + notificationMessage.getData() != null ? notificationMessage.getData().get("notification_type") : null); + + logger.debug("Notification payload: {}", notificationMessage); + + try { + String response = firebaseNotificationService.sendNotification(notificationMessage); + + logger.info("Notification processed successfully. Response={}", response); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + + logger.error("Failed to process notification request. Token={}, ReceiverId={}, Error={}", + notificationMessage.getToken(), + notificationMessage.getData() != null ? notificationMessage.getData().get("receiver_user_id") : null, + e.getMessage(), + e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to send notification"); + } } - @RequestMapping(value = "updateToken",method = RequestMethod.POST,headers = "Authorization") - public String updateToken(@RequestBody UserToken userToken){ - return firebaseNotificationService.updateToken(userToken); + @PostMapping("/updateToken") + public ResponseEntity updateToken(@RequestBody UserToken userToken) { + try { + + Object result = firebaseNotificationService.updateToken(userToken); + + return ResponseEntity.ok(result); + + } catch (IllegalArgumentException e) { + + return ResponseEntity.badRequest().body(e.getMessage()); + + } catch (Exception e) { + + logger.error("Error while updating Firebase token", e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to update Firebase token."); + } } - @RequestMapping(value = "getToken",method = RequestMethod.GET,headers = "Authorization") + @PostMapping(value = "getToken") public String getUserToken() throws IEMRException { return firebaseNotificationService.getUserToken(); 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 2ae06257..d127fe3c 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -82,6 +82,7 @@ public class IEMRAdminController { private static final String USER_ID_FIELD = "userId"; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -172,7 +173,17 @@ public String userAuthenticate( } String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword()); - List mUser = iemrAdminUserServiceImpl.userAuthenticate(m_User.getUserName(), decryptPassword); + + + List mUser = iemrAdminUserServiceImpl + .userAuthenticate(m_User.getUserName(), decryptPassword); + + + User loggedInUser = mUser.get(0); + + loggedInUser.setFailedAttempt(0); + + iemrAdminUserServiceImpl.save(loggedInUser); JSONObject resMap = new JSONObject(); JSONObject serviceRoleMultiMap = new JSONObject(); JSONObject serviceRoleMap = new JSONObject(); @@ -181,11 +192,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()); @@ -254,7 +276,6 @@ public String userAuthenticate( // Facility data for ALL users - common pattern, empty if not applicable try { if (mUser.size() == 1) { - User loggedInUser = mUser.get(0); String userRoleName = ""; if (loggedInUser.getM_UserServiceRoleMapping() != null) { for (UserServiceRoleMapping usrm : loggedInUser.getM_UserServiceRoleMapping()) { @@ -413,16 +434,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 = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + stringRedisTemplate.delete("jti:" + usernameKey); } - stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); @@ -537,11 +570,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()); @@ -1000,6 +1035,13 @@ public String userLogout(HttpServletRequest request) { try { deleteSessionObjectByGettingSessionDetails(request.getHeader("Authorization")); sessionObject.deleteSessionObject(request.getHeader("Authorization")); + try { + stringRedisTemplate.delete("camp:vanID"); + stringRedisTemplate.delete("camp:parkingPlaceID"); + logger.info("Camp config cleared from Redis on MMU logout"); + } catch (Exception redisEx) { + logger.warn("Failed to clear camp Redis keys on logout: {}", redisEx.getMessage()); + } response.setResponse("Success"); } catch (Exception e) { response.setError(e); diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java index 8cfeb0de..05f06df2 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java @@ -27,6 +27,9 @@ public class FormFieldOption { @Column(name = "label_as") private String labelAs; + @Column(name = "label_bn") + private String labelBn; + @Column(name = "sort_order") private Integer sortOrder; 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 52cb8027..949ad20e 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -20,6 +20,8 @@ public class Translation { private String hindiTranslation; @Column(name = "assamese_translation") private String assameseTranslation; + @Column(name = "bengali_translation") + private String bengaliTranslation; @Column(name = "is_active") private Boolean isActive; diff --git a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java b/src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java similarity index 77% rename from src/main/java/com/iemr/common/data/userToken/UserTokenData.java rename to src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java index 0646972e..0a0fefbe 100644 --- a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java +++ b/src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java @@ -24,23 +24,24 @@ */ package com.iemr.common.data.userToken; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import java.sql.Timestamp; +import static jakarta.persistence.GenerationType.IDENTITY; + @Entity -@Table(name = "user_tokens", schema = "db_iemr") +@Table(name = "user_fcm_tokens", schema = "db_iemr") @Data -public class UserTokenData { +public class UserFcmTokenData { @Id + @GeneratedValue(strategy = IDENTITY) + private int id; @Column(name = "user_id") - Integer userId; + private Integer userId; @Column(name = "token") - String token; + private String token; @Column(name = "updated_at") - Timestamp updatedAt; + private Timestamp updatedAt; } diff --git a/src/main/java/com/iemr/common/dto/identity/Address.java b/src/main/java/com/iemr/common/dto/identity/Address.java index 7a5e563f..b857f21d 100644 --- a/src/main/java/com/iemr/common/dto/identity/Address.java +++ b/src/main/java/com/iemr/common/dto/identity/Address.java @@ -52,6 +52,12 @@ private String parkingPlaceName; private Integer servicePointID; private String servicePointName; + private Double gpsLatitude; + private Double gpsLongitude; + private String digipin; + private Long gpsTimestamp; + private Boolean isGpsUnavailable; + private String gpsUnavailableReason; public static Address bendemographicsAddressMapper(BeneficiaryDemographicsModel i_bendemographics) { Address address = new Address(); @@ -87,6 +93,12 @@ public static Address bendemographicsAddressMapper(BeneficiaryDemographicsModel address.setSubDistrictId(i_bendemographics.getBlockID()); address.setVillageId(i_bendemographics.getDistrictBranchID()); address.setPinCode(i_bendemographics.getPinCode()); + address.setGpsLatitude(i_bendemographics.getLatitude()); + address.setGpsLongitude(i_bendemographics.getLongitude()); + address.setDigipin(i_bendemographics.getDigipin()); + address.setGpsTimestamp(i_bendemographics.getGpsTimestamp()); + address.setIsGpsUnavailable(i_bendemographics.getIsGpsUnavailable()); + address.setGpsUnavailableReason(i_bendemographics.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java b/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java index 7d263b25..8b29ec15 100644 --- a/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java @@ -158,6 +158,12 @@ public BeneficiaryDemographicsModel createBenDemographicsModel(BeneficiariesDTO demographicsModel.setServicePointID(address.getServicePointID()); demographicsModel.setServicePointName(address.getServicePointName()); demographicsModel.setHabitation(address.getHabitation()); + demographicsModel.setLatitude(address.getGpsLatitude()); + demographicsModel.setLongitude(address.getGpsLongitude()); + demographicsModel.setDigipin(address.getDigipin()); + demographicsModel.setGpsTimestamp(address.getGpsTimestamp()); + demographicsModel.setIsGpsUnavailable(address.getIsGpsUnavailable()); + demographicsModel.setGpsUnavailableReason(address.getGpsUnavailableReason()); } demographicsModel.setHealthCareWorkerID(benDetails.getHealthCareWorkerId()); demographicsModel.setPreferredLangID(beneficiariesDTO.getBeneficiaryDetails().getPreferredLanguageId()); @@ -216,6 +222,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java index 1480f306..f18920ad 100644 --- a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java @@ -248,6 +248,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java index e0f3ef1c..27b4a602 100644 --- a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java @@ -107,6 +107,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java index 83450844..044650ea 100644 --- a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java +++ b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java @@ -173,6 +173,19 @@ public class BeneficiaryDemographicsModel { @Expose private String habitation; + @Expose + private Double latitude; + @Expose + private Double longitude; + @Expose + private String digipin; + @Expose + private Long gpsTimestamp; + @Expose + private Boolean isGpsUnavailable = false; + @Expose + private String gpsUnavailableReason; + @Expose private String occupation; @Expose diff --git a/src/main/java/com/iemr/common/model/notification/NotificationMessage.java b/src/main/java/com/iemr/common/model/notification/NotificationMessage.java index 58343034..55cb0e62 100644 --- a/src/main/java/com/iemr/common/model/notification/NotificationMessage.java +++ b/src/main/java/com/iemr/common/model/notification/NotificationMessage.java @@ -35,4 +35,4 @@ public class NotificationMessage { private String title; private String body; private Map data; -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java similarity index 78% rename from src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java rename to src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java index 817f26f0..b2ed14df 100644 --- a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java +++ b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java @@ -24,8 +24,15 @@ */ package com.iemr.common.repo.userToken; -import com.iemr.common.data.userToken.UserTokenData; +import com.iemr.common.data.userToken.UserFcmTokenData; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; -public interface UserTokenRepo extends JpaRepository { +import java.util.Optional; + +@Repository + +public interface UserFcmTokenRepo extends JpaRepository { + + Optional findByUserId(Integer userId); } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java index 0bf1c2fe..38154f4a 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -160,6 +160,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedLabel = label.getEnglish(); + }else if ("bn".equalsIgnoreCase(lang)) { + translatedLabel = label.getBengaliTranslation(); + } } @@ -171,6 +174,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedPlaceHolder = placeHolder.getEnglish(); + } else if ("bn".equalsIgnoreCase(lang)) { + translatedPlaceHolder = placeHolder.getBengaliTranslation(); + } } @@ -203,7 +209,8 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str map.put("value", opt.getValue()); if ("hi".equalsIgnoreCase(lang)) map.put("label", opt.getLabelHi()); else if ("as".equalsIgnoreCase(lang)) map.put("label", opt.getLabelAs()); - else map.put("label", opt.getLabelEn()); + else if("en".equals(lang)) map.put("label", opt.getLabelEn()); + else if("bn".equals(lang)) map.put("label", opt.getLabelBn()); return map; }) .collect(Collectors.toList()); diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 7f33c453..66140ccc 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -26,12 +26,15 @@ import com.google.firebase.FirebaseException; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; -import com.iemr.common.data.userToken.UserTokenData; +import com.google.gson.Gson; +import com.iemr.common.config.firebase.FirebaseMessagingConfig; +import com.iemr.common.data.userToken.UserFcmTokenData; import com.iemr.common.model.notification.NotificationMessage; import com.iemr.common.model.notification.UserToken; -import com.iemr.common.repo.userToken.UserTokenRepo; +import com.iemr.common.repo.userToken.UserFcmTokenRepo; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; import com.iemr.common.utils.exception.IEMRException; @@ -39,11 +42,11 @@ 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 org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import java.sql.Timestamp; import java.util.Optional; @Service @@ -51,10 +54,10 @@ public class FirebaseNotificationService { final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired(required = false) - FirebaseMessaging firebaseMessaging; + private FirebaseMessaging firebaseMessaging; @Autowired - private UserTokenRepo userTokenRepo; + private UserFcmTokenRepo userTokenRepo; @Autowired private CookieUtil cookieUtil; @@ -64,42 +67,77 @@ public class FirebaseNotificationService { private Message message; + @Value("${firebase.enabled}") + private boolean firebaseEnabled; + + @Value("${firebase.credential-file}") + private String firebaseCredentialFile; + + public String sendNotification(NotificationMessage notificationMessage) { + + logger.info("===== Initializing Firebase ====="); + logger.info("firebaseEnabled={}", firebaseEnabled); + logger.info("firebaseCredentialFile={}", firebaseCredentialFile); + + logger.info("========== FCM Notification Request =========="); + logger.info("Request : {}", new Gson().toJson(notificationMessage)); + if (firebaseMessaging == null) { - logger.error("⚠️ Firebase is not configured, skipping notification"); - return null; + logger.error("FirebaseMessaging bean is not initialized."); + return "FirebaseMessaging bean is not initialized."; } - Notification notification = Notification.builder().setTitle(notificationMessage.getTitle()).setBody(notificationMessage.getBody()).build(); + try { - Message message = Message.builder().setTopic(notificationMessage.getToken()).setNotification(notification).putAllData(notificationMessage.getData()).build(); + Notification notification = Notification.builder() + .setTitle(notificationMessage.getTitle()) + .setBody(notificationMessage.getBody()) + .build(); + Message message = Message.builder() + .setToken(notificationMessage.getToken()) + .setNotification(notification) + .putAllData(notificationMessage.getData()) + .build(); - try { - String response = FirebaseMessaging.getInstance().send(message); + logger.info("Sending notification to token: {}", notificationMessage.getToken()); + + String response = firebaseMessaging.send(message); + + logger.info("Notification sent successfully."); + logger.info("Firebase Message Id : {}", response); return response; - } catch (FirebaseException e) { - return "Error sending notification"; + } catch (FirebaseMessagingException e) { + + logger.error("FirebaseMessagingException"); + logger.error("Error Code : {}", e.getMessagingErrorCode()); + logger.error("Error Message : {}", e.getMessage(), e); + + return "FCM Error : " + e.getMessage(); + + } catch (Exception e) { + + logger.error("Unexpected exception while sending notification.", e); + + return "Unexpected Error : " + e.getMessage(); } } - public String updateToken(UserToken userToken) { - Optional existingTokenData = userTokenRepo.findById(userToken.getUserId()); + Optional existingTokenData = userTokenRepo.findByUserId(userToken.getUserId()); - UserTokenData userTokenData; + UserFcmTokenData userTokenData; if (existingTokenData.isPresent()) { userTokenData = existingTokenData.get(); userTokenData.setToken(userToken.getToken()); - userTokenData.setUpdatedAt(new Timestamp(System.currentTimeMillis())); } else { - userTokenData = new UserTokenData(); + userTokenData = new UserFcmTokenData(); userTokenData.setUserId(userToken.getUserId()); userTokenData.setToken(userToken.getToken()); - userTokenData.setUpdatedAt(new Timestamp(System.currentTimeMillis())); } userTokenRepo.save(userTokenData); @@ -111,7 +149,7 @@ public String getUserToken() throws IEMRException { .getRequest(); String jwtTokenFromCookie = cookieUtil.getJwtTokenFromCookie(requestHeader); return userTokenRepo.findById(Integer.parseInt(jwtUtil.getUserIdFromToken(jwtTokenFromCookie))) // because your userId is Long in DB - .map(UserTokenData::getToken) + .map(UserFcmTokenData::getToken) .orElse(null); // } diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 7714efce..0c905d0a 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -72,7 +72,6 @@ public class HealthService { private static final String STATUS_VALUE = "Value"; private static final String STATUS_UP = "UP"; private static final String STATUS_DOWN = "DOWN"; - private static final String STATUS_DEGRADED = "DEGRADED"; private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; // Thresholds @@ -183,9 +182,11 @@ private Map checkDatabaseConnectivity() { stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution stmt.execute("SELECT 1"); - // If SELECT 1 succeeds, use cached severity from background diagnostics + // SELECT 1 succeeded, so connectivity is UP regardless of background severity. + // Severity is a separate capacity/performance signal (pool usage, long transactions, + // etc.) — it must not be reported as a connectivity outage. String severity = cachedDbSeverity.get(); - result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_STATUS, STATUS_UP); result.put(FIELD_SEVERITY, severity); } catch (Exception e) { @@ -402,13 +403,6 @@ private String performConnectionUsageCheck(Connection conn) { } return SEVERITY_OK; } - private String resolveDatabaseStatus(String severity) { - return switch (severity) { - case SEVERITY_CRITICAL -> STATUS_DOWN; - case SEVERITY_WARNING -> STATUS_DEGRADED; - default -> STATUS_UP; - }; - } private String escalate(String current, String candidate) { return severityRank(candidate) > severityRank(current) ? candidate : current; } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index a366fa0c..cbd29cf1 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -133,4 +133,5 @@ public List getUserServiceRoleMappingForProvider(Integ boolean hasAdminPrivileges(Long userId) throws IEMRException; + User save(User loggedInUser); } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index f9624f13..e411b9ef 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -308,7 +308,13 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr user.setFailedAttempt(currentAttempts + 1); iEMRUserRepositoryCustom.save(user); logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); + int remainingAttempts = failedAttemptThreshold - (currentAttempts + 1); + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + throw new IEMRException("Invalid username or password. Remaining attempts: " + remainingAttempts); } else { java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); user.setFailedAttempt(currentAttempts + 1); @@ -317,7 +323,8 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr iEMRUserRepositoryCustom.save(user); logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", failedAttemptThreshold); - throw new IEMRException(generateLockoutErrorMessage(lockTime)); + throw new IEMRException( + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } @@ -1377,4 +1384,9 @@ public boolean hasAdminPrivileges(Long userId) throws IEMRException { return false; } } + + @Override + public User save(User loggedInUser) { + return iEMRUserRepositoryCustom.save(loggedInUser); + } } diff --git a/src/main/java/com/iemr/common/utils/CookieUtil.java b/src/main/java/com/iemr/common/utils/CookieUtil.java index 92c071c5..1562e3c4 100644 --- a/src/main/java/com/iemr/common/utils/CookieUtil.java +++ b/src/main/java/com/iemr/common/utils/CookieUtil.java @@ -35,8 +35,8 @@ public void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, H // Make the cookie HttpOnly to prevent JavaScript access for security cookie.setHttpOnly(true); - // Set the Max-Age (expiry time) in seconds (8 hours) - cookie.setMaxAge(60 * 60 * 8); // 8 hours expiration + // Set the Max-Age (expiry time) in seconds (10 hours) + cookie.setMaxAge(60 * 60 * 10); // 10 hours expiration // Set the path to "/" so the cookie is available across the entire application cookie.setPath("/"); diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 557d5da5..3be36bbf 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -168,7 +168,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from cookie"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromCookie)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } @@ -176,7 +176,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from header"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromHeader)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } diff --git a/src/main/java/com/iemr/common/utils/NetworkUtil.java b/src/main/java/com/iemr/common/utils/NetworkUtil.java new file mode 100644 index 00000000..b5ded4d5 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/NetworkUtil.java @@ -0,0 +1,56 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import java.net.DatagramSocket; +import java.net.InetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves the single LAN-facing IPv4 address the OS would use for outbound + * traffic, used to build a URL that a device on the same wifi network (e.g. + * a mobile phone) can connect to. A machine can have several network + * interfaces (wifi, ethernet, VPN, virtual adapters); connecting a UDP + * socket to an external address - without sending any packet - makes the OS + * routing table pick the one real outbound interface, avoiding ambiguity + * from iterating all interfaces. + */ +public final class NetworkUtil { + + private static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class); + private static final String FALLBACK_IP = "127.0.0.1"; + + private NetworkUtil() { + } + + public static String getLanIPAddress() { + try (DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + return socket.getLocalAddress().getHostAddress(); + } catch (Exception e) { + logger.error("Failed to resolve LAN IP address", e); + return FALLBACK_IP; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a2de342..bc7a8c14 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -177,7 +177,7 @@ account.lock.duration.hours=24 #Jwt Token configuration jwt.access.expiration=28800000 -jwt.refresh.expiration=604800000 +jwt.refresh.expiration=7776000000 # local env