diff --git a/pom.xml b/pom.xml index d19b7dc..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 @@ -472,6 +471,12 @@ repackage + + build-info + + build-info + + @@ -485,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 new file mode 100644 index 0000000..a9759dc --- /dev/null +++ b/src/main/java/com/iemr/admin/config/SecurityFilterConfig.java @@ -0,0 +1,33 @@ +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("/*"); + registration.setOrder(1); + + // Set name for easier debugging + registration.setName("JwtUserIdValidationFilter"); + + 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..36fa4e6 --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -0,0 +1,60 @@ +/* +* 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.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 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() { + 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 fa8f0a6..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,58 +21,40 @@ */ package com.iemr.admin.controller.version; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - 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 }) - public String versionInformation() { - OutputResponse output = new OutputResponse(); - try { - logger.info("version Controller Start"); - output.setResponse(readGitProperties()); - } catch (Exception e) { - output.setError(e); - } - - logger.info("version Controller End"); - return output.toString(); - } - - private String readGitProperties() throws Exception { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream inputStream = classLoader.getResourceAsStream("git.properties"); - - return readFromInputStream(inputStream); - } - - 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"); - } - } - return resultStringBuilder.toString(); - } -} + 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..473d37f --- /dev/null +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -0,0 +1,188 @@ +/* +* 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.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisCallback; +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; + + @Value("${app.version:unknown}") + private String appVersion; + + @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", appVersion); + + 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("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("responseTime", responseTime + "ms"); + dbStatus.put("error", e.getClass().getSimpleName()); + + logger.error("Database health check failed: {}", e.getMessage(), e); + } + + return dbStatus; + } + + private Map checkRedisHealth() { + Map redisStatus = new HashMap<>(); + long startTime = System.currentTimeMillis(); + + try { + // 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; + + // 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; + } +} \ 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 64c89a9..6a7f5fd 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -6,7 +6,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import com.iemr.admin.utils.http.AuthorizationHeaderRequestWrapper; import jakarta.servlet.Filter; @@ -36,6 +35,17 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; + String path = request.getRequestURI(); + 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; + } + String origin = request.getHeader("Origin"); if (origin != null && isOriginAllowed(origin)) { response.setHeader("Access-Control-Allow-Origin", origin); @@ -52,8 +62,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - String path = request.getRequestURI(); - String contextPath = request.getContextPath(); logger.info("JwtUserIdValidationFilter invoked for path: " + path); // Log cookies for debugging @@ -122,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()); } } @@ -177,4 +182,4 @@ private void clearUserIdCookie(HttpServletResponse response) { cookie.setMaxAge(0); // Invalidate the cookie response.addCookie(cookie); } -} +} \ No newline at end of file