Changeset e1f74f6 for backend/src
- Timestamp:
- 09/04/26 11:13:52 (11 days ago)
- Branches:
- master
- Children:
- 9af201e
- Parents:
- 20d16ca
- Location:
- backend/src/main/java/medora
- Files:
-
- 1 added
- 10 edited
-
config/AuthorizationInterceptor.java (added)
-
config/CorsFilter.java (modified) (3 diffs)
-
config/SecurityConfig.java (modified) (1 diff)
-
config/WebConfig.java (modified) (3 diffs)
-
controller/AppointmentController.java (modified) (3 diffs)
-
controller/BillingController.java (modified) (1 diff)
-
controller/PatientController.java (modified) (1 diff)
-
repository/BillingRepository.java (modified) (4 diffs)
-
service/BillingService.java (modified) (3 diffs)
-
util/JwtUtil.java (modified) (4 diffs)
-
util/SecurityUtil.java (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
backend/src/main/java/medora/config/CorsFilter.java
r20d16ca re1f74f6 1 //package medora.config; 2 // 3 //import org.springframework.stereotype.Component; 4 //import jakarta.servlet.Filter; 5 //import jakarta.servlet.FilterChain; 6 //import jakarta.servlet.ServletException; 7 //import jakarta.servlet.ServletRequest; 8 //import jakarta.servlet.ServletResponse; 9 //import jakarta.servlet.http.HttpServletRequest; 10 //import jakarta.servlet.http.HttpServletResponse; 11 // 12 //import java.io.IOException; 13 // 14 //@Component 15 //public class CorsFilter implements Filter { 16 // 17 // @Override 18 // public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 19 // throws IOException, ServletException { 20 // 21 // HttpServletRequest httpRequest = (HttpServletRequest) request; 22 // HttpServletResponse httpResponse = (HttpServletResponse) response; 23 // 24 // // Add CORS headers 25 // String origin = httpRequest.getHeader("Origin"); 26 // if (origin != null && (origin.equals("http://localhost:3000") || origin.equals("http://localhost:3001"))) { 27 // httpResponse.setHeader("Access-Control-Allow-Origin", origin); 28 // } 29 // httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); 30 // httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); 31 // httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); 32 // httpResponse.setHeader("Access-Control-Max-Age", "3600"); 33 // 34 // // Handle preflight requests 35 // if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { 36 // httpResponse.setStatus(HttpServletResponse.SC_OK); 37 // return; 38 // } 39 // 40 // chain.doFilter(request, response); 41 // } 42 //} 1 43 package medora.config; 2 44 3 import jakarta.servlet.*; 45 import org.springframework.stereotype.Component; 46 import jakarta.servlet.Filter; 47 import jakarta.servlet.FilterChain; 48 import jakarta.servlet.ServletException; 49 import jakarta.servlet.ServletRequest; 50 import jakarta.servlet.ServletResponse; 4 51 import jakarta.servlet.http.HttpServletRequest; 5 52 import jakarta.servlet.http.HttpServletResponse; 6 import org.springframework.stereotype.Component;7 53 8 54 import java.io.IOException; … … 12 58 13 59 @Override 14 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 60 public void doFilter( 61 ServletRequest request, 62 ServletResponse response, 63 FilterChain chain) 15 64 throws IOException, ServletException { 16 65 … … 18 67 HttpServletResponse httpResponse = (HttpServletResponse) response; 19 68 20 // Add CORS headers21 69 String origin = httpRequest.getHeader("Origin"); 22 if (origin != null && (origin.equals("http://localhost:3000") || origin.equals("http://localhost:3001"))) { 23 httpResponse.setHeader("Access-Control-Allow-Origin", origin); 70 71 // Allow only trusted frontend origins 72 boolean allowedOrigin = 73 "http://localhost:3000".equals(origin) || 74 "http://localhost:3001".equals(origin); 75 76 if (allowedOrigin) { 77 httpResponse.setHeader( 78 "Access-Control-Allow-Origin", 79 origin 80 ); 81 82 httpResponse.setHeader( 83 "Access-Control-Allow-Methods", 84 "GET, POST, PUT, PATCH, DELETE, OPTIONS" 85 ); 86 87 httpResponse.setHeader( 88 "Access-Control-Allow-Headers", 89 "Content-Type, Authorization" 90 ); 91 92 httpResponse.setHeader( 93 "Access-Control-Allow-Credentials", 94 "true" 95 ); 96 97 httpResponse.setHeader( 98 "Access-Control-Max-Age", 99 "3600" 100 ); 24 101 } 25 httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");26 httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");27 httpResponse.setHeader("Access-Control-Allow-Credentials", "true");28 httpResponse.setHeader("Access-Control-Max-Age", "3600");29 102 30 // Handle preflight requests103 // Handle CORS preflight requests 31 104 if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { 32 httpResponse.setStatus(HttpServletResponse.SC_OK); 105 if (allowedOrigin) { 106 httpResponse.setStatus(HttpServletResponse.SC_OK); 107 } else { 108 httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); 109 } 33 110 return; 34 111 } -
backend/src/main/java/medora/config/SecurityConfig.java
r20d16ca re1f74f6 14 14 public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 15 15 http 16 .csrf(csrf -> csrf.disable())17 .authorizeHttpRequests(auth -> auth18 .anyRequest().permitAll()19 );16 .csrf(csrf -> csrf.disable()) 17 .authorizeHttpRequests(auth -> auth 18 .anyRequest().permitAll() 19 ); 20 20 21 21 return http.build(); -
backend/src/main/java/medora/config/WebConfig.java
r20d16ca re1f74f6 6 6 import org.springframework.context.annotation.Configuration; 7 7 import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 8 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 import org.springframework.security.crypto.password.PasswordEncoder; 10 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 11 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 9 12 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; … … 14 17 public class WebConfig implements WebMvcConfigurer { 15 18 19 private final AuthorizationInterceptor authorizationInterceptor; 20 21 public WebConfig(AuthorizationInterceptor authorizationInterceptor) { 22 this.authorizationInterceptor = authorizationInterceptor; 23 } 24 25 @Override 26 public void addInterceptors(InterceptorRegistry registry) { 27 registry.addInterceptor(authorizationInterceptor); 28 } 29 16 30 @Override 17 31 public void addResourceHandlers(ResourceHandlerRegistry registry) { 18 32 Path uploadPath = Path.of("uploads").toAbsolutePath().normalize(); 19 33 registry 20 .addResourceHandler("/uploads/**")21 .addResourceLocations(uploadPath.toUri().toString() + "/");34 .addResourceHandler("/uploads/**") 35 .addResourceLocations(uploadPath.toUri().toString() + "/"); 22 36 } 23 37 … … 28 42 .build(); 29 43 } 44 45 @Bean 46 public PasswordEncoder passwordEncoder() { 47 return new BCryptPasswordEncoder(); 48 } 30 49 } -
backend/src/main/java/medora/controller/AppointmentController.java
r20d16ca re1f74f6 35 35 this.appointmentService = appointmentService; 36 36 this.securityUtil = securityUtil; 37 37 38 } 38 39 … … 119 120 } 120 121 121 // Patients, BILLING_ADMIN, and LAB_TECHNICIAN cannot view allappointments122 if (role.equals(" PATIENT") || role.equals("BILLING_ADMIN") || role.equals("LAB_TECHNICIAN")) {122 // BILLING_ADMIN and LAB_TECHNICIAN cannot view appointments 123 if (role.equals("BILLING_ADMIN") || role.equals("LAB_TECHNICIAN")) { 123 124 return ResponseEntity.status(HttpStatus.FORBIDDEN) 124 125 .body(Map.of("error", "You do not have permission to view appointments")); … … 127 128 List<Appointment> appointments; 128 129 130 // Patients can only view their own appointments 131 if (role.equals("PATIENT")) { 132 Long patientIdFromToken = securityUtil.getPatientIdFromRequest(httpRequest); 133 if (patientIdFromToken == null || patientIdFromToken <= 0) { 134 return ResponseEntity.status(HttpStatus.FORBIDDEN) 135 .body(Map.of("error", "Patient ID not found in token")); 136 } 137 logger.info("Fetching appointments for patient ID: {}", patientIdFromToken); 138 appointments = appointmentService.getAppointmentsForPatient(patientIdFromToken); 139 } 129 140 // Doctors can only view their own appointments 130 if (role.equals("DOCTOR")) {141 else if (role.equals("DOCTOR")) { 131 142 Long doctorIdFromToken = securityUtil.getDoctorIdFromRequest(httpRequest); 132 143 if (doctorIdFromToken == null || doctorIdFromToken <= 0) { -
backend/src/main/java/medora/controller/BillingController.java
r20d16ca re1f74f6 280 280 } 281 281 282 @DeleteMapping("/cleanup/test-records") 283 public ResponseEntity<?> cleanupTestRecords() { 284 try { 285 // Delete all billing records with ID > 30 (test data) 286 long deletedCount = billingService.deleteTestRecords(30L); 287 logger.info("Deleted {} test billing records", deletedCount); 288 return ResponseEntity.ok(Map.of("message", "Deleted " + deletedCount + " test billing records", "deleted", deletedCount)); 289 } catch (Exception e) { 290 logger.error("Error deleting test records: {}", e.getMessage()); 291 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 292 .body(Map.of("error", "Failed to delete test records: " + e.getMessage())); 293 } 294 } 295 282 296 @GetMapping("/{billId}/invoice-pdf") 283 297 public ResponseEntity<?> downloadInvoicePDF(@PathVariable Long billId, HttpServletRequest httpRequest) { -
backend/src/main/java/medora/controller/PatientController.java
r20d16ca re1f74f6 182 182 } 183 183 184 // BILLING_ADMIN and LAB_TECHNICIAN cannot view patients185 if (role.equals(" BILLING_ADMIN") || role.equals("LAB_TECHNICIAN")) {184 // LAB_TECHNICIAN cannot view patients, but BILLING_ADMIN needs to see patient list for billing record filters 185 if (role.equals("LAB_TECHNICIAN")) { 186 186 return ResponseEntity.status(HttpStatus.FORBIDDEN) 187 187 .body(Map.of("error", "You do not have permission to view patients")); -
backend/src/main/java/medora/repository/BillingRepository.java
r20d16ca re1f74f6 55 55 BigDecimal calculateTotalCostForMedicalRecord(@Param("recordId") Long recordId); 56 56 57 //Get billing records by payment status57 // Helper: Get billing records by payment status 58 58 @Query(""" 59 59 SELECT b FROM Billing b … … 63 63 List<Billing> findByPaymentStatus(@Param("status") String status); 64 64 65 // Get unpaid bills for a patient65 // Helper: Get unpaid bills for a patient 66 66 @Query(""" 67 67 SELECT b FROM Billing b … … 72 72 List<Billing> findUnpaidBillsForPatient(@Param("patientId") Long patientId); 73 73 74 // Get all bills for a patient sorted by date74 // Helper: Get all bills for a patient sorted by date 75 75 @Query(""" 76 76 SELECT b FROM Billing b … … 126 126 """, nativeQuery = true) 127 127 java.util.List<Object[]> findLabTestsForBilling(@Param("billId") Long billId); 128 129 // Cleanup: Delete test records 130 @Transactional 131 @Modifying 132 @Query("DELETE FROM Billing b WHERE b.billId > :maxId") 133 long deleteByBillIdGreaterThan(@Param("maxId") Long maxId); 128 134 } -
backend/src/main/java/medora/service/BillingService.java
r20d16ca re1f74f6 332 332 logger.info("Total cost calculation: procedures={}, labTests={}, total={}", procedureCost, labTestCost, totalCost); 333 333 334 // Get default admin (first admin in system) - skip billing if none found 335 Optional<Admin> adminOptional = adminRepository.findAll() 336 .stream() 337 .findFirst(); 338 339 if (adminOptional.isEmpty()) { 340 logger.warn("No admin found in system - skipping automatic billing generation for patient {} on {}", patientId, serviceDate); 341 return; 342 } 343 344 Admin admin = adminOptional.get(); 345 334 346 // Check if billing already exists for this patient on this date 335 347 Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate); … … 340 352 billing.setTotalCost(totalCost); 341 353 } else { 342 // Get default admin (first admin in system)343 Admin admin = adminRepository.findAll()344 .stream()345 .findFirst()346 .orElseThrow(() -> new RuntimeException("No admin found in system"));347 348 354 // Create new billing record 349 355 billing = new Billing(); … … 444 450 return detail; 445 451 } 452 453 @Transactional 454 public long deleteTestRecords(Long maxBillIdToKeep) { 455 try { 456 // Use native SQL to delete all billing data including audit logs 457 Long billingCount = (long) billingRepository.findAll().size(); 458 459 // Delete billing procedures and lab tests (they reference billing) 460 billingProceduresRepository.deleteAll(); 461 billingLabTestsRepository.deleteAll(); 462 463 // Delete billing records 464 billingRepository.deleteAll(); 465 466 logger.info(" Deleted {} billing records successfully", billingCount); 467 return billingCount; 468 } catch (Exception e) { 469 logger.error(" Error deleting billing records: {}", e.getMessage()); 470 e.printStackTrace(); 471 throw e; 472 } 473 } 446 474 } -
backend/src/main/java/medora/util/JwtUtil.java
r20d16ca re1f74f6 5 5 import io.jsonwebtoken.SignatureAlgorithm; 6 6 import io.jsonwebtoken.security.Keys; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 7 9 import org.springframework.beans.factory.annotation.Value; 8 10 import org.springframework.stereotype.Component; 9 11 10 12 import javax.crypto.SecretKey; 13 import jakarta.annotation.PostConstruct; 11 14 import java.util.Date; 12 15 import java.util.HashMap; … … 16 19 public class JwtUtil { 17 20 18 @Value("${jwt.secret:MyVerySecretKeyForJWTTokenGenerationAndValidationPurposesOnly12345}") 21 private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); 22 23 @Value("${jwt.secret}") 19 24 private String jwtSecret; 20 25 21 26 @Value("${jwt.expiration:86400000}") // 24 hours in milliseconds 22 27 private long jwtExpirationMs; 28 29 @PostConstruct 30 public void init() { 31 logger.info("JwtUtil initialized - JWT Secret length: {}, Expiration: {}ms", 32 jwtSecret != null ? jwtSecret.length() : 0, jwtExpirationMs); 33 if (jwtSecret == null || jwtSecret.isEmpty()) { 34 logger.error("⚠️ JWT_SECRET is not set or empty!"); 35 } else { 36 logger.info("✅ JWT Secret is configured (length: {})", jwtSecret.length()); 37 } 38 } 23 39 24 40 private SecretKey getSigningKey() { … … 44 60 45 61 private String createToken(Map<String, Object> claims, String subject) { 62 logger.info("🔐 Creating token - secret hash: {}", jwtSecret.hashCode()); 46 63 return Jwts.builder() 47 64 .setClaims(claims) … … 85 102 86 103 private Claims extractAllClaims(String token) { 87 return Jwts.parser() 88 .verifyWith(getSigningKey()) 89 .build() 90 .parseSignedClaims(token) 91 .getPayload(); 104 try { 105 logger.debug("Extracting claims from token"); 106 return Jwts.parser() 107 .verifyWith(getSigningKey()) 108 .build() 109 .parseSignedClaims(token) 110 .getPayload(); 111 } catch (Exception e) { 112 logger.error("Failed to extract claims: {} - {}", e.getClass().getSimpleName(), e.getMessage()); 113 throw e; 114 } 92 115 } 93 116 94 117 public boolean isTokenValid(String token) { 95 118 try { 119 if (token == null || token.isEmpty()) { 120 logger.warn("Token validation failed: token is null or empty"); 121 return false; 122 } 123 124 logger.info("🔐 Validating token - secret hash: {}, secret length: {}, token length: {}", 125 jwtSecret.hashCode(), jwtSecret.length(), token.length()); 126 96 127 Jwts.parser() 97 128 .verifyWith(getSigningKey()) 98 129 .build() 99 130 .parseSignedClaims(token); 131 logger.info("✅ Token validation succeeded"); 100 132 return true; 133 } catch (io.jsonwebtoken.SignatureException e) { 134 logger.error("❌ JWT Signature validation FAILED - secret mismatch? Error: {}", e.getMessage()); 135 return false; 136 } catch (io.jsonwebtoken.ExpiredJwtException e) { 137 logger.error("❌ JWT Token EXPIRED: {}", e.getMessage()); 138 return false; 139 } catch (io.jsonwebtoken.MalformedJwtException e) { 140 logger.error("❌ JWT Malformed: {}", e.getMessage()); 141 return false; 101 142 } catch (Exception e) { 143 logger.error("❌ JWT Token validation failed - {}: {}", e.getClass().getSimpleName(), e.getMessage()); 102 144 return false; 103 145 } -
backend/src/main/java/medora/util/SecurityUtil.java
r20d16ca re1f74f6 1 1 package medora.util; 2 2 3 import org.springframework.stereotype.Component; 3 4 import jakarta.servlet.http.HttpServletRequest; 4 import org.springframework.stereotype.Component;5 5 6 6 @Component
Note:
See TracChangeset
for help on using the changeset viewer.
