From e71943a4a4533aac258beb7adc625ab7a30ada11 Mon Sep 17 00:00:00 2001 From: Suraj Date: Fri, 1 Aug 2025 21:55:58 +0530 Subject: [PATCH 1/4] Implement /health and /version endpoints for Admin API --- pom.xml | 36 ++++++ .../admin/config/SecurityFilterConfig.java | 32 +++++ .../controller/health/HealthController.java | 40 ++++++ .../controller/version/VersionController.java | 118 +++++++++++++++--- .../utils/JwtUserIdValidationFilter.java | 72 +++++++++-- 5 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/iemr/admin/config/SecurityFilterConfig.java create mode 100644 src/main/java/com/iemr/admin/controller/health/HealthController.java diff --git a/pom.xml b/pom.xml index d19b7dc..545d3b2 100644 --- a/pom.xml +++ b/pom.xml @@ -462,6 +462,42 @@ + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + + get-the-git-infos + + revision + + initialize + + + + true + ${project.build.outputDirectory}/git.properties + + git.commit.id + git.build.time + + full + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + org.springframework.boot spring-boot-maven-plugin diff --git a/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java b/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java new file mode 100644 index 0000000..4fd1c92 --- /dev/null +++ b/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java @@ -0,0 +1,32 @@ +package com.iemr.admin.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.iemr.admin.utils.JwtAuthenticationUtil; +import com.iemr.admin.utils.JwtUserIdValidationFilter; + +@Configuration +public class SecurityFilterConfig { + + @Autowired + private JwtAuthenticationUtil jwtAuthenticationUtil; + + @Value("${cors.allowed-origins}") + private String allowedOrigins; + + @Bean + public FilterRegistrationBean jwtFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new JwtUserIdValidationFilter(jwtAuthenticationUtil, allowedOrigins)); + registration.addUrlPatterns("/*"); + + // Exclude health and version endpoints + registration.addInitParameter("excludedUrls", "/health,/version"); + + return registration; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/controller/health/HealthController.java b/src/main/java/com/iemr/admin/controller/health/HealthController.java new file mode 100644 index 0000000..53f7aa5 --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -0,0 +1,40 @@ +/* +* 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.admin.controller.healthcheck; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +public class HealthController { + + @GetMapping("/health") + public ResponseEntity> health() { + Map response = new HashMap<>(); + response.put("status", "UP"); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/controller/version/VersionController.java b/src/main/java/com/iemr/admin/controller/version/VersionController.java index fa8f0a6..5bb0ded 100644 --- a/src/main/java/com/iemr/admin/controller/version/VersionController.java +++ b/src/main/java/com/iemr/admin/controller/version/VersionController.java @@ -21,14 +21,16 @@ */ package com.iemr.admin.controller.version; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Properties; +import java.util.TimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -44,35 +46,115 @@ public class VersionController { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); @Operation(summary = "Version information") - @RequestMapping(value = "/version", method = { RequestMethod.GET }) + @RequestMapping(value = "/version", method = { RequestMethod.GET }, produces = MediaType.APPLICATION_JSON_VALUE) public String versionInformation() { OutputResponse output = new OutputResponse(); try { logger.info("version Controller Start"); - output.setResponse(readGitProperties()); + // Set the version information as JSON string directly + output.setResponse(readGitPropertiesAsJson()); } catch (Exception e) { output.setError(e); } logger.info("version Controller End"); + + // Use standard toString() - no custom formatting return output.toString(); } - private String readGitProperties() throws Exception { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream inputStream = classLoader.getResourceAsStream("git.properties"); - - return readFromInputStream(inputStream); + private String readGitPropertiesAsJson() throws Exception { + StringBuilder json = new StringBuilder(); + json.append("{\n"); + + // Read Git properties + Properties gitProps = loadPropertiesFile("git.properties"); + if (gitProps != null) { + // For git.commit.id, look for both standard and abbrev versions + String commitId = gitProps.getProperty("git.commit.id", null); + if (commitId == null) { + commitId = gitProps.getProperty("git.commit.id.abbrev", "unknown"); + } + json.append(" \"git.commit.id\": \"").append(commitId).append("\",\n"); + + // For git.build.time, look for various possible property names + String buildTime = gitProps.getProperty("git.build.time", null); + if (buildTime == null) { + buildTime = gitProps.getProperty("git.commit.time", null); + } + if (buildTime == null) { + buildTime = gitProps.getProperty("git.commit.timestamp", "unknown"); + } + json.append(" \"git.build.time\": \"").append(buildTime).append("\",\n"); + } else { + logger.warn("git.properties file not found. Git information will be unavailable."); + json.append(" \"git.commit.id\": \"information unavailable\",\n"); + json.append(" \"git.build.time\": \"information unavailable\",\n"); + } + + // Read build properties if available + Properties buildProps = loadPropertiesFile("META-INF/build-info.properties"); + if (buildProps != null) { + // Extract version - checking for both standard and nested formats + String version = buildProps.getProperty("build.version", null); + if (version == null) { + version = buildProps.getProperty("build.version.number", null); + } + if (version == null) { + version = buildProps.getProperty("version", "unknown"); + } + json.append(" \"build.version\": \"").append(version).append("\",\n"); + + // Extract time - checking for both standard and alternate formats + String time = buildProps.getProperty("build.time", null); + if (time == null) { + time = buildProps.getProperty("build.timestamp", null); + } + if (time == null) { + time = buildProps.getProperty("timestamp", "unknown"); + } + json.append(" \"build.time\": \"").append(time).append("\",\n"); + } else { + logger.info("build-info.properties not found, trying Maven properties"); + // Fallback to maven project version + Properties mavenProps = loadPropertiesFile("META-INF/maven/com.iemr.admin/admin-api/pom.properties"); + if (mavenProps != null) { + String version = mavenProps.getProperty("version", "unknown"); + json.append(" \"build.version\": \"").append(version).append("\",\n"); + json.append(" \"build.time\": \"").append(getCurrentIstTimeFormatted()).append("\",\n"); + } else { + logger.warn("Neither build-info.properties nor Maven properties found."); + json.append(" \"build.version\": \"3.1.0\",\n"); // Default version + json.append(" \"build.time\": \"").append(getCurrentIstTimeFormatted()).append("\",\n"); + } + } + json.append(" \"current.time\": \"").append(getCurrentIstTimeFormatted()).append("\"\n"); + + json.append(" }"); + return json.toString(); + } + + /** + * Get the current time formatted in Indian Standard Time (IST) + * IST is UTC+5:30 + */ + private String getCurrentIstTimeFormatted() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata")); + return sdf.format(new Date()); } - private String readFromInputStream(InputStream inputStream) throws IOException { - StringBuilder resultStringBuilder = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) { - String line; - while ((line = br.readLine()) != null) { - resultStringBuilder.append(line).append("\n"); + private Properties loadPropertiesFile(String resourceName) { + ClassLoader classLoader = getClass().getClassLoader(); + try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { + if (inputStream != null) { + Properties props = new Properties(); + props.load(inputStream); + return props; } + } catch (IOException e) { + logger.warn("Could not load properties file: " + resourceName, e); } - return resultStringBuilder.toString(); + return null; } -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index 64c89a9..8bad8a0 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -5,12 +5,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import org.springframework.beans.factory.annotation.Autowired; import com.iemr.admin.utils.http.AuthorizationHeaderRequestWrapper; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; @@ -20,21 +21,64 @@ public class JwtUserIdValidationFilter implements Filter { - private final JwtAuthenticationUtil jwtAuthenticationUtil; + private JwtAuthenticationUtil jwtAuthenticationUtil; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private final String allowedOrigins; - + private String allowedOrigins; + + // FilterConfig for storing initialization parameters + private FilterConfig filterConfig; + + // Add no-args constructor for bean creation in FilterRegistrationBean + public JwtUserIdValidationFilter() { + // Default constructor for Spring to instantiate + this.allowedOrigins = "*"; + } + public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, String allowedOrigins) { this.jwtAuthenticationUtil = jwtAuthenticationUtil; this.allowedOrigins = allowedOrigins; } + + // Store FilterConfig during initialization + @Override + public void init(FilterConfig filterConfig) throws ServletException { + this.filterConfig = filterConfig; + } + + // Method to check if a URL is in the excluded list + private boolean isExcludedUrl(String path) { + if (filterConfig == null) { + return false; + } + + String excludedUrls = filterConfig.getInitParameter("excludedUrls"); + if (excludedUrls != null) { + String[] urls = excludedUrls.split(","); + for (String url : urls) { + if (path.equals(url.trim())) { + logger.info("Skipping JWT validation for excluded URL: {}", path); + return true; + } + } + } + return false; + } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Check for excluded URLs first + String path = request.getRequestURI(); + if (isExcludedUrl(path) || + path.equals("/health") || + path.equals("/version")) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } String origin = request.getHeader("Origin"); if (origin != null && isOriginAllowed(origin)) { @@ -52,7 +96,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - String path = request.getRequestURI(); String contextPath = request.getContextPath(); logger.info("JwtUserIdValidationFilter invoked for path: " + path); @@ -89,7 +132,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String jwtFromHeader = request.getHeader(Constants.JWT_TOKEN); String authHeader = request.getHeader("Authorization"); - if (jwtFromCookie != null) { + if (jwtFromCookie != null && jwtAuthenticationUtil != null) { logger.info("Validating JWT token from cookie"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromCookie)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( @@ -97,7 +140,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } - } else if (jwtFromHeader != null) { + } else if (jwtFromHeader != null && jwtAuthenticationUtil != null) { logger.info("Validating JWT token from header"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromHeader)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( @@ -177,4 +220,17 @@ private void clearUserIdCookie(HttpServletResponse response) { cookie.setMaxAge(0); // Invalidate the cookie response.addCookie(cookie); } -} + + // Setter methods for Spring to inject dependencies + @Autowired(required = false) + public void setJwtAuthenticationUtil(JwtAuthenticationUtil jwtAuthenticationUtil) { + this.jwtAuthenticationUtil = jwtAuthenticationUtil; + } + + @Autowired(required = false) + public void setAllowedOrigins(String allowedOrigins) { + if (allowedOrigins != null) { + this.allowedOrigins = allowedOrigins; + } + } +} \ No newline at end of file From 50bbaa028e6370737b6580d9d8bc1654ae1d4ece Mon Sep 17 00:00:00 2001 From: Suraj Date: Thu, 7 Aug 2025 11:10:46 +0530 Subject: [PATCH 2/4] fix: changes for /health and /version endpoints --- pom.xml | 59 ++---- .../admin/config/SecurityFilterConfig.java | 5 +- .../controller/health/HealthController.java | 34 ++- .../controller/version/VersionController.java | 144 ++----------- .../admin/service/health/HealthService.java | 198 ++++++++++++++++++ .../admin/service/version/VersionService.java | 154 ++++++++++++++ .../utils/JwtUserIdValidationFilter.java | 77 ++----- 7 files changed, 431 insertions(+), 240 deletions(-) create mode 100644 src/main/java/com/iemr/admin/service/health/HealthService.java create mode 100644 src/main/java/com/iemr/admin/service/version/VersionService.java diff --git a/pom.xml b/pom.xml index 545d3b2..1d61364 100644 --- a/pom.xml +++ b/pom.xml @@ -295,26 +295,25 @@ pl.project13.maven git-commit-id-plugin - 2.2.4 + 4.9.10 get-the-git-infos revision + initialize - ${project.basedir}/.git - git - false true - - ${project.build.outputDirectory}/git.properties - json - - true - + ${project.build.outputDirectory}/git.properties + + git.commit.id + git.build.time + + full + properties @@ -462,50 +461,20 @@ - - - pl.project13.maven - git-commit-id-plugin - 4.9.10 - - - get-the-git-infos - - revision - - initialize - - - - true - ${project.build.outputDirectory}/git.properties - - git.commit.id - git.build.time - - full - - - org.springframework.boot spring-boot-maven-plugin + 3.2.2 - build-info + repackage - - - - org.springframework.boot - spring-boot-maven-plugin - 3.2.2 - + build-info - repackage + build-info @@ -521,4 +490,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java b/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java index 4fd1c92..a9759dc 100644 --- a/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java +++ b/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java @@ -23,9 +23,10 @@ public FilterRegistrationBean jwtFilterRegistration() FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(new JwtUserIdValidationFilter(jwtAuthenticationUtil, allowedOrigins)); registration.addUrlPatterns("/*"); + registration.setOrder(1); - // Exclude health and version endpoints - registration.addInitParameter("excludedUrls", "/health,/version"); + // Set name for easier debugging + registration.setName("JwtUserIdValidationFilter"); return registration; } diff --git a/src/main/java/com/iemr/admin/controller/health/HealthController.java b/src/main/java/com/iemr/admin/controller/health/HealthController.java index 53f7aa5..36fa4e6 100644 --- a/src/main/java/com/iemr/admin/controller/health/HealthController.java +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -19,22 +19,42 @@ * 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.admin.controller.healthcheck; +package com.iemr.admin.controller.health; +import java.util.Map; + +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.GetMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.HashMap; -import java.util.Map; +import com.iemr.admin.service.health.HealthService; + +import io.swagger.v3.oas.annotations.Operation; @RestController public class HealthController { + private static final Logger logger = LoggerFactory.getLogger(HealthController.class); + + @Autowired + private HealthService healthService; + + @Operation(summary = "Health check endpoint") @GetMapping("/health") - public ResponseEntity> health() { - Map response = new HashMap<>(); - response.put("status", "UP"); - return ResponseEntity.ok(response); + public ResponseEntity> health() { + logger.info("Health check endpoint called"); + + Map healthStatus = healthService.checkHealth(); + + // Return 503 if any service is down, 200 if all are up + String status = (String) healthStatus.get("status"); + HttpStatus httpStatus = "UP".equals(status) ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; + + logger.info("Health check completed with status: {}", status); + return ResponseEntity.status(httpStatus).body(healthStatus); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/controller/version/VersionController.java b/src/main/java/com/iemr/admin/controller/version/VersionController.java index 5bb0ded..b48214e 100644 --- a/src/main/java/com/iemr/admin/controller/version/VersionController.java +++ b/src/main/java/com/iemr/admin/controller/version/VersionController.java @@ -21,140 +21,40 @@ */ package com.iemr.admin.controller.version; -import java.io.IOException; -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Properties; -import java.util.TimeZone; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import com.iemr.admin.service.version.VersionService; import com.iemr.admin.utils.response.OutputResponse; import io.swagger.v3.oas.annotations.Operation; - @RestController public class VersionController { - private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - - @Operation(summary = "Version information") - @RequestMapping(value = "/version", method = { RequestMethod.GET }, produces = MediaType.APPLICATION_JSON_VALUE) - public String versionInformation() { - OutputResponse output = new OutputResponse(); - try { - logger.info("version Controller Start"); - // Set the version information as JSON string directly - output.setResponse(readGitPropertiesAsJson()); - } catch (Exception e) { - output.setError(e); - } - - logger.info("version Controller End"); - - // Use standard toString() - no custom formatting - return output.toString(); - } - - private String readGitPropertiesAsJson() throws Exception { - StringBuilder json = new StringBuilder(); - json.append("{\n"); - - // Read Git properties - Properties gitProps = loadPropertiesFile("git.properties"); - if (gitProps != null) { - // For git.commit.id, look for both standard and abbrev versions - String commitId = gitProps.getProperty("git.commit.id", null); - if (commitId == null) { - commitId = gitProps.getProperty("git.commit.id.abbrev", "unknown"); - } - json.append(" \"git.commit.id\": \"").append(commitId).append("\",\n"); - - // For git.build.time, look for various possible property names - String buildTime = gitProps.getProperty("git.build.time", null); - if (buildTime == null) { - buildTime = gitProps.getProperty("git.commit.time", null); - } - if (buildTime == null) { - buildTime = gitProps.getProperty("git.commit.timestamp", "unknown"); - } - json.append(" \"git.build.time\": \"").append(buildTime).append("\",\n"); - } else { - logger.warn("git.properties file not found. Git information will be unavailable."); - json.append(" \"git.commit.id\": \"information unavailable\",\n"); - json.append(" \"git.build.time\": \"information unavailable\",\n"); - } - - // Read build properties if available - Properties buildProps = loadPropertiesFile("META-INF/build-info.properties"); - if (buildProps != null) { - // Extract version - checking for both standard and nested formats - String version = buildProps.getProperty("build.version", null); - if (version == null) { - version = buildProps.getProperty("build.version.number", null); - } - if (version == null) { - version = buildProps.getProperty("version", "unknown"); - } - json.append(" \"build.version\": \"").append(version).append("\",\n"); - - // Extract time - checking for both standard and alternate formats - String time = buildProps.getProperty("build.time", null); - if (time == null) { - time = buildProps.getProperty("build.timestamp", null); - } - if (time == null) { - time = buildProps.getProperty("timestamp", "unknown"); - } - json.append(" \"build.time\": \"").append(time).append("\",\n"); - } else { - logger.info("build-info.properties not found, trying Maven properties"); - // Fallback to maven project version - Properties mavenProps = loadPropertiesFile("META-INF/maven/com.iemr.admin/admin-api/pom.properties"); - if (mavenProps != null) { - String version = mavenProps.getProperty("version", "unknown"); - json.append(" \"build.version\": \"").append(version).append("\",\n"); - json.append(" \"build.time\": \"").append(getCurrentIstTimeFormatted()).append("\",\n"); - } else { - logger.warn("Neither build-info.properties nor Maven properties found."); - json.append(" \"build.version\": \"3.1.0\",\n"); // Default version - json.append(" \"build.time\": \"").append(getCurrentIstTimeFormatted()).append("\",\n"); - } - } - json.append(" \"current.time\": \"").append(getCurrentIstTimeFormatted()).append("\"\n"); - - json.append(" }"); - return json.toString(); - } - - /** - * Get the current time formatted in Indian Standard Time (IST) - * IST is UTC+5:30 - */ - private String getCurrentIstTimeFormatted() { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata")); - return sdf.format(new Date()); - } - - private Properties loadPropertiesFile(String resourceName) { - ClassLoader classLoader = getClass().getClassLoader(); - try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { - if (inputStream != null) { - Properties props = new Properties(); - props.load(inputStream); - return props; - } - } catch (IOException e) { - logger.warn("Could not load properties file: " + resourceName, e); - } - return null; - } + private static final Logger logger = LoggerFactory.getLogger(VersionController.class); + + @Autowired + private VersionService versionService; + + @Operation(summary = "Version information") + @RequestMapping(value = "/version", method = { RequestMethod.GET }, produces = MediaType.APPLICATION_JSON_VALUE) + public String versionInformation() { + OutputResponse output = new OutputResponse(); + try { + logger.info("version Controller Start"); + output.setResponse(versionService.getVersionInformation()); + } catch (Exception e) { + logger.error("Error in version controller", e); + output.setError(e); + } + + logger.info("version Controller End"); + return output.toString(); + } } \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java new file mode 100644 index 0000000..cdecea6 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -0,0 +1,198 @@ +/* +* 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.admin.service.health; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class HealthService { + + private static final Logger logger = LoggerFactory.getLogger(HealthService.class); + private static final String DB_HEALTH_CHECK_QUERY = "SELECT 1 as health_check"; + + @Autowired + private DataSource dataSource; + + @Autowired(required = false) + private RedisTemplate redisTemplate; + + public Map checkHealth() { + Map healthStatus = new HashMap<>(); + Map services = new HashMap<>(); + boolean overallHealth = true; + + // Check database connectivity + Map dbStatus = checkDatabaseHealth(); + services.put("database", dbStatus); + if (!"UP".equals(dbStatus.get("status"))) { + overallHealth = false; + } + + // Check Redis connectivity if configured + if (redisTemplate != null) { + Map redisStatus = checkRedisHealth(); + services.put("redis", redisStatus); + if (!"UP".equals(redisStatus.get("status"))) { + overallHealth = false; + } + } else { + Map redisStatus = new HashMap<>(); + redisStatus.put("status", "NOT_CONFIGURED"); + redisStatus.put("message", "Redis not configured for this environment"); + services.put("redis", redisStatus); + } + + healthStatus.put("status", overallHealth ? "UP" : "DOWN"); + healthStatus.put("services", services); + healthStatus.put("timestamp", Instant.now().toString()); + healthStatus.put("application", "admin-api"); + healthStatus.put("version", "3.1.0"); + + logger.info("Health check completed - Overall status: {}", overallHealth ? "UP" : "DOWN"); + return healthStatus; + } + + private Map checkDatabaseHealth() { + Map dbStatus = new HashMap<>(); + long startTime = System.currentTimeMillis(); + + try (Connection connection = dataSource.getConnection()) { + // Test connection validity + boolean isConnectionValid = connection.isValid(5); // 5 second timeout + + if (isConnectionValid) { + // Execute a simple query to ensure database is responsive + try (PreparedStatement stmt = connection.prepareStatement(DB_HEALTH_CHECK_QUERY); + ResultSet rs = stmt.executeQuery()) { + + if (rs.next() && rs.getInt(1) == 1) { + long responseTime = System.currentTimeMillis() - startTime; + + dbStatus.put("status", "UP"); + dbStatus.put("database", connection.getMetaData().getDatabaseProductName()); + dbStatus.put("driver", connection.getMetaData().getDriverName()); + dbStatus.put("url", sanitizeUrl(connection.getMetaData().getURL())); + dbStatus.put("responseTime", responseTime + "ms"); + dbStatus.put("message", "Database connection successful"); + + logger.debug("Database health check: UP ({}ms)", responseTime); + } else { + dbStatus.put("status", "DOWN"); + dbStatus.put("message", "Database query returned unexpected result"); + logger.warn("Database health check: Query returned unexpected result"); + } + } + } else { + dbStatus.put("status", "DOWN"); + dbStatus.put("message", "Database connection is not valid"); + logger.warn("Database health check: Connection is not valid"); + } + + } catch (Exception e) { + long responseTime = System.currentTimeMillis() - startTime; + dbStatus.put("status", "DOWN"); + dbStatus.put("message", "Database connection failed: " + e.getMessage()); + dbStatus.put("responseTime", responseTime + "ms"); + dbStatus.put("error", e.getClass().getSimpleName()); + + logger.error("Database health check failed", e); + } + + return dbStatus; + } + + private Map checkRedisHealth() { + Map redisStatus = new HashMap<>(); + long startTime = System.currentTimeMillis(); + + try { + // Test Redis connection with ping + String pong = redisTemplate.getConnectionFactory().getConnection().ping(); + + if ("PONG".equals(pong)) { + long responseTime = System.currentTimeMillis() - startTime; + + // Additional test: set and get a test key + String testKey = "health:check:" + System.currentTimeMillis(); + String testValue = "test-value"; + + redisTemplate.opsForValue().set(testKey, testValue); + Object retrievedValue = redisTemplate.opsForValue().get(testKey); + redisTemplate.delete(testKey); // Clean up test key + + if (testValue.equals(retrievedValue)) { + redisStatus.put("status", "UP"); + redisStatus.put("responseTime", responseTime + "ms"); + redisStatus.put("message", "Redis connection and operations successful"); + redisStatus.put("ping", "PONG"); + + logger.debug("Redis health check: UP ({}ms)", responseTime); + } else { + redisStatus.put("status", "DOWN"); + redisStatus.put("message", "Redis set/get operation failed"); + logger.warn("Redis health check: Set/Get operation failed"); + } + } else { + redisStatus.put("status", "DOWN"); + redisStatus.put("message", "Redis ping returned: " + pong); + logger.warn("Redis health check: Ping returned unexpected response: {}", pong); + } + + } catch (Exception e) { + long responseTime = System.currentTimeMillis() - startTime; + redisStatus.put("status", "DOWN"); + redisStatus.put("message", "Redis connection failed: " + e.getMessage()); + redisStatus.put("responseTime", responseTime + "ms"); + redisStatus.put("error", e.getClass().getSimpleName()); + + logger.error("Redis health check failed", e); + } + + return redisStatus; + } + + /** + * Sanitize database URL to remove sensitive information like passwords + */ + private String sanitizeUrl(String url) { + if (url == null) { + return "unknown"; + } + + // Remove password parameter if present + return url.replaceAll("password=[^&]*", "password=***") + .replaceAll("pwd=[^&]*", "pwd=***"); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/service/version/VersionService.java b/src/main/java/com/iemr/admin/service/version/VersionService.java new file mode 100644 index 0000000..8562816 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/version/VersionService.java @@ -0,0 +1,154 @@ +/* +* 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.admin.service.version; + +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class VersionService { + + private static final Logger logger = LoggerFactory.getLogger(VersionService.class); + + @Value("${app.version:unknown}") + private String appVersion; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String getVersionInformation() { + try { + Map versionInfo = buildVersionInfo(); + return objectMapper.writeValueAsString(versionInfo); + } catch (Exception e) { + logger.error("Error building version information", e); + return createErrorResponse(); + } + } + + private Map buildVersionInfo() { + Map versionInfo = new LinkedHashMap<>(); + + // Add Git information + addGitInformation(versionInfo); + + // Add build information + addBuildInformation(versionInfo); + + // Add current time + versionInfo.put("current.time", getCurrentIstTimeFormatted()); + + return versionInfo; + } + + private void addGitInformation(Map versionInfo) { + Properties gitProps = loadPropertiesFile("git.properties"); + if (gitProps != null) { + String commitId = gitProps.getProperty("git.commit.id", + gitProps.getProperty("git.commit.id.abbrev", "unknown")); + versionInfo.put("git.commit.id", commitId); + + String buildTime = gitProps.getProperty("git.build.time", + gitProps.getProperty("git.commit.time", + gitProps.getProperty("git.commit.timestamp", "unknown"))); + versionInfo.put("git.build.time", buildTime); + } else { + logger.warn("git.properties file not found. Git information will be unavailable."); + versionInfo.put("git.commit.id", "information unavailable"); + versionInfo.put("git.build.time", "information unavailable"); + } + } + + private void addBuildInformation(Map versionInfo) { + Properties buildProps = loadPropertiesFile("META-INF/build-info.properties"); + if (buildProps != null) { + String version = buildProps.getProperty("build.version", + buildProps.getProperty("build.version.number", + buildProps.getProperty("version", appVersion))); + versionInfo.put("build.version", version); + + String time = buildProps.getProperty("build.time", + buildProps.getProperty("build.timestamp", + buildProps.getProperty("timestamp", getCurrentIstTimeFormatted()))); + versionInfo.put("build.time", time); + } else { + logger.info("build-info.properties not found, trying Maven properties"); + Properties mavenProps = loadPropertiesFile("META-INF/maven/com.iemr.admin/admin-api/pom.properties"); + if (mavenProps != null) { + String version = mavenProps.getProperty("version", appVersion); + versionInfo.put("build.version", version); + versionInfo.put("build.time", getCurrentIstTimeFormatted()); + } else { + logger.warn("Neither build-info.properties nor Maven properties found."); + versionInfo.put("build.version", appVersion); + versionInfo.put("build.time", getCurrentIstTimeFormatted()); + } + } + } + + private String getCurrentIstTimeFormatted() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata")); + return sdf.format(new Date()); + } + + private Properties loadPropertiesFile(String resourceName) { + ClassLoader classLoader = getClass().getClassLoader(); + try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { + if (inputStream != null) { + Properties props = new Properties(); + props.load(inputStream); + return props; + } + } catch (IOException e) { + logger.warn("Could not load properties file: " + resourceName, e); + } + return null; + } + + private String createErrorResponse() { + try { + Map errorInfo = new LinkedHashMap<>(); + errorInfo.put("git.commit.id", "error retrieving information"); + errorInfo.put("git.build.time", "error retrieving information"); + errorInfo.put("build.version", appVersion); + errorInfo.put("build.time", getCurrentIstTimeFormatted()); + errorInfo.put("current.time", getCurrentIstTimeFormatted()); + return objectMapper.writeValueAsString(errorInfo); + } catch (Exception e) { + logger.error("Error creating error response", e); + return "{\"error\": \"Unable to retrieve version information\"}"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index 8bad8a0..6a7f5fd 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -5,13 +5,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import com.iemr.admin.utils.http.AuthorizationHeaderRequestWrapper; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; @@ -21,61 +19,29 @@ public class JwtUserIdValidationFilter implements Filter { - private JwtAuthenticationUtil jwtAuthenticationUtil; + private final JwtAuthenticationUtil jwtAuthenticationUtil; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private String allowedOrigins; - - // FilterConfig for storing initialization parameters - private FilterConfig filterConfig; - - // Add no-args constructor for bean creation in FilterRegistrationBean - public JwtUserIdValidationFilter() { - // Default constructor for Spring to instantiate - this.allowedOrigins = "*"; - } - + private final String allowedOrigins; + public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, String allowedOrigins) { this.jwtAuthenticationUtil = jwtAuthenticationUtil; this.allowedOrigins = allowedOrigins; } - - // Store FilterConfig during initialization - @Override - public void init(FilterConfig filterConfig) throws ServletException { - this.filterConfig = filterConfig; - } - - // Method to check if a URL is in the excluded list - private boolean isExcludedUrl(String path) { - if (filterConfig == null) { - return false; - } - - String excludedUrls = filterConfig.getInitParameter("excludedUrls"); - if (excludedUrls != null) { - String[] urls = excludedUrls.split(","); - for (String url : urls) { - if (path.equals(url.trim())) { - logger.info("Skipping JWT validation for excluded URL: {}", path); - return true; - } - } - } - return false; - } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; - - // Check for excluded URLs first + String path = request.getRequestURI(); - if (isExcludedUrl(path) || - path.equals("/health") || - path.equals("/version")) { + String contextPath = request.getContextPath(); + + // FIRST: Check for health and version endpoints - skip ALL processing + if (path.equals("/health") || path.equals("/version") || + path.equals(contextPath + "/health") || path.equals(contextPath + "/version")) { + logger.info("Skipping JWT validation for monitoring endpoint: {}", path); filterChain.doFilter(servletRequest, servletResponse); return; } @@ -96,7 +62,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - String contextPath = request.getContextPath(); logger.info("JwtUserIdValidationFilter invoked for path: " + path); // Log cookies for debugging @@ -132,7 +97,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String jwtFromHeader = request.getHeader(Constants.JWT_TOKEN); String authHeader = request.getHeader("Authorization"); - if (jwtFromCookie != null && jwtAuthenticationUtil != null) { + if (jwtFromCookie != null) { logger.info("Validating JWT token from cookie"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromCookie)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( @@ -140,7 +105,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } - } else if (jwtFromHeader != null && jwtAuthenticationUtil != null) { + } else if (jwtFromHeader != null) { logger.info("Validating JWT token from header"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromHeader)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( @@ -165,12 +130,9 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.warn("No valid authentication token found"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Invalid or missing token"); - logger.warn("No valid authentication token found"); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Invalid or missing token"); - } catch (Exception e) { logger.error("Authorization error: ", e); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization error: "); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization error: " + e.getMessage()); } } @@ -220,17 +182,4 @@ private void clearUserIdCookie(HttpServletResponse response) { cookie.setMaxAge(0); // Invalidate the cookie response.addCookie(cookie); } - - // Setter methods for Spring to inject dependencies - @Autowired(required = false) - public void setJwtAuthenticationUtil(JwtAuthenticationUtil jwtAuthenticationUtil) { - this.jwtAuthenticationUtil = jwtAuthenticationUtil; - } - - @Autowired(required = false) - public void setAllowedOrigins(String allowedOrigins) { - if (allowedOrigins != null) { - this.allowedOrigins = allowedOrigins; - } - } } \ No newline at end of file From a3734822ef99aae0b4a9029a6d059b4365644b34 Mon Sep 17 00:00:00 2001 From: Suraj Date: Thu, 7 Aug 2025 22:59:23 +0530 Subject: [PATCH 3/4] changes for /health and /version. --- .../iemr/admin/service/health/HealthService.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index cdecea6..adcaeda 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -33,6 +33,8 @@ 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.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -45,6 +47,9 @@ public class HealthService { @Autowired private DataSource dataSource; + @Value("${app.version:unknown}") + private String appVersion; + @Autowired(required = false) private RedisTemplate redisTemplate; @@ -78,7 +83,7 @@ public Map checkHealth() { healthStatus.put("services", services); healthStatus.put("timestamp", Instant.now().toString()); healthStatus.put("application", "admin-api"); - healthStatus.put("version", "3.1.0"); + healthStatus.put("version", appVersion); logger.info("Health check completed - Overall status: {}", overallHealth ? "UP" : "DOWN"); return healthStatus; @@ -138,8 +143,10 @@ private Map checkRedisHealth() { long startTime = System.currentTimeMillis(); try { - // Test Redis connection with ping - String pong = redisTemplate.getConnectionFactory().getConnection().ping(); + // Test Redis connection with ping using explicit RedisCallback + String pong = redisTemplate.execute((RedisCallback) connection -> { + return connection.ping(); + }); if ("PONG".equals(pong)) { long responseTime = System.currentTimeMillis() - startTime; From bded2992a9407282783e02e3a1d3f6b1594357a0 Mon Sep 17 00:00:00 2001 From: Suraj Date: Tue, 12 Aug 2025 22:11:54 +0530 Subject: [PATCH 4/4] fix: remove sensitive database metadata from health endpoint --- .../admin/service/health/HealthService.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index adcaeda..473d37f 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -106,9 +106,6 @@ private Map checkDatabaseHealth() { long responseTime = System.currentTimeMillis() - startTime; dbStatus.put("status", "UP"); - dbStatus.put("database", connection.getMetaData().getDatabaseProductName()); - dbStatus.put("driver", connection.getMetaData().getDriverName()); - dbStatus.put("url", sanitizeUrl(connection.getMetaData().getURL())); dbStatus.put("responseTime", responseTime + "ms"); dbStatus.put("message", "Database connection successful"); @@ -128,11 +125,10 @@ private Map checkDatabaseHealth() { } catch (Exception e) { long responseTime = System.currentTimeMillis() - startTime; dbStatus.put("status", "DOWN"); - dbStatus.put("message", "Database connection failed: " + e.getMessage()); dbStatus.put("responseTime", responseTime + "ms"); dbStatus.put("error", e.getClass().getSimpleName()); - logger.error("Database health check failed", e); + logger.error("Database health check failed: {}", e.getMessage(), e); } return dbStatus; @@ -189,17 +185,4 @@ private Map checkRedisHealth() { return redisStatus; } - - /** - * Sanitize database URL to remove sensitive information like passwords - */ - private String sanitizeUrl(String url) { - if (url == null) { - return "unknown"; - } - - // Remove password parameter if present - return url.replaceAll("password=[^&]*", "password=***") - .replaceAll("pwd=[^&]*", "pwd=***"); - } } \ No newline at end of file