Ignore:
Timestamp:
09/04/26 11:13:52 (12 days ago)
Author:
MBK <marija.karapandzova@…>
Branches:
master
Children:
9af201e
Parents:
20d16ca
Message:

Fix authentication, authorization, performance, and add security documentation

Location:
backend/src/main/java/medora
Files:
1 added
10 edited

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//}
    143package medora.config;
    244
    3 import jakarta.servlet.*;
     45import org.springframework.stereotype.Component;
     46import jakarta.servlet.Filter;
     47import jakarta.servlet.FilterChain;
     48import jakarta.servlet.ServletException;
     49import jakarta.servlet.ServletRequest;
     50import jakarta.servlet.ServletResponse;
    451import jakarta.servlet.http.HttpServletRequest;
    552import jakarta.servlet.http.HttpServletResponse;
    6 import org.springframework.stereotype.Component;
    753
    854import java.io.IOException;
     
    1258
    1359    @Override
    14     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
     60    public void doFilter(
     61            ServletRequest request,
     62            ServletResponse response,
     63            FilterChain chain)
    1564            throws IOException, ServletException {
    1665
     
    1867        HttpServletResponse httpResponse = (HttpServletResponse) response;
    1968
    20         // Add CORS headers
    2169        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            );
    24101        }
    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");
    29102
    30         // Handle preflight requests
     103        // Handle CORS preflight requests
    31104        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            }
    33110            return;
    34111        }
  • backend/src/main/java/medora/config/SecurityConfig.java

    r20d16ca re1f74f6  
    1414    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    1515        http
    16             .csrf(csrf -> csrf.disable())
    17             .authorizeHttpRequests(auth -> auth
    18                 .anyRequest().permitAll()
    19             );
     16                .csrf(csrf -> csrf.disable())
     17                .authorizeHttpRequests(auth -> auth
     18                        .anyRequest().permitAll()
     19                );
    2020
    2121        return http.build();
  • backend/src/main/java/medora/config/WebConfig.java

    r20d16ca re1f74f6  
    66import org.springframework.context.annotation.Configuration;
    77import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
     8import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
     9import org.springframework.security.crypto.password.PasswordEncoder;
     10import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    811import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    912import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
     
    1417public class WebConfig implements WebMvcConfigurer {
    1518
     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
    1630    @Override
    1731    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    1832        Path uploadPath = Path.of("uploads").toAbsolutePath().normalize();
    1933        registry
    20             .addResourceHandler("/uploads/**")
    21             .addResourceLocations(uploadPath.toUri().toString() + "/");
     34                .addResourceHandler("/uploads/**")
     35                .addResourceLocations(uploadPath.toUri().toString() + "/");
    2236    }
    2337
     
    2842                .build();
    2943    }
     44
     45    @Bean
     46    public PasswordEncoder passwordEncoder() {
     47        return new BCryptPasswordEncoder();
     48    }
    3049}
  • backend/src/main/java/medora/controller/AppointmentController.java

    r20d16ca re1f74f6  
    3535        this.appointmentService = appointmentService;
    3636        this.securityUtil = securityUtil;
     37
    3738    }
    3839
     
    119120            }
    120121
    121             // Patients, BILLING_ADMIN, and LAB_TECHNICIAN cannot view all appointments
    122             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")) {
    123124                return ResponseEntity.status(HttpStatus.FORBIDDEN)
    124125                        .body(Map.of("error", "You do not have permission to view appointments"));
     
    127128            List<Appointment> appointments;
    128129
     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            }
    129140            // Doctors can only view their own appointments
    130             if (role.equals("DOCTOR")) {
     141            else if (role.equals("DOCTOR")) {
    131142                Long doctorIdFromToken = securityUtil.getDoctorIdFromRequest(httpRequest);
    132143                if (doctorIdFromToken == null || doctorIdFromToken <= 0) {
  • backend/src/main/java/medora/controller/BillingController.java

    r20d16ca re1f74f6  
    280280    }
    281281
     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
    282296    @GetMapping("/{billId}/invoice-pdf")
    283297    public ResponseEntity<?> downloadInvoicePDF(@PathVariable Long billId, HttpServletRequest httpRequest) {
  • backend/src/main/java/medora/controller/PatientController.java

    r20d16ca re1f74f6  
    182182            }
    183183
    184             // BILLING_ADMIN and LAB_TECHNICIAN cannot view patients
    185             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")) {
    186186                return ResponseEntity.status(HttpStatus.FORBIDDEN)
    187187                        .body(Map.of("error", "You do not have permission to view patients"));
  • backend/src/main/java/medora/repository/BillingRepository.java

    r20d16ca re1f74f6  
    5555    BigDecimal calculateTotalCostForMedicalRecord(@Param("recordId") Long recordId);
    5656
    57    //Get billing records by payment status
     57    // Helper: Get billing records by payment status
    5858    @Query("""
    5959        SELECT b FROM Billing b
     
    6363    List<Billing> findByPaymentStatus(@Param("status") String status);
    6464
    65     // Get unpaid bills for a patient
     65    // Helper: Get unpaid bills for a patient
    6666    @Query("""
    6767        SELECT b FROM Billing b
     
    7272    List<Billing> findUnpaidBillsForPatient(@Param("patientId") Long patientId);
    7373
    74     //Get all bills for a patient sorted by date
     74    // Helper: Get all bills for a patient sorted by date
    7575    @Query("""
    7676        SELECT b FROM Billing b
     
    126126    """, nativeQuery = true)
    127127    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);
    128134}
  • backend/src/main/java/medora/service/BillingService.java

    r20d16ca re1f74f6  
    332332            logger.info("Total cost calculation: procedures={}, labTests={}, total={}", procedureCost, labTestCost, totalCost);
    333333
     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
    334346            // Check if billing already exists for this patient on this date
    335347            Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate);
     
    340352                billing.setTotalCost(totalCost);
    341353            } 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 
    348354                // Create new billing record
    349355                billing = new Billing();
     
    444450        return detail;
    445451    }
     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    }
    446474}
  • backend/src/main/java/medora/util/JwtUtil.java

    r20d16ca re1f74f6  
    55import io.jsonwebtoken.SignatureAlgorithm;
    66import io.jsonwebtoken.security.Keys;
     7import org.slf4j.Logger;
     8import org.slf4j.LoggerFactory;
    79import org.springframework.beans.factory.annotation.Value;
    810import org.springframework.stereotype.Component;
    911
    1012import javax.crypto.SecretKey;
     13import jakarta.annotation.PostConstruct;
    1114import java.util.Date;
    1215import java.util.HashMap;
     
    1619public class JwtUtil {
    1720
    18     @Value("${jwt.secret:MyVerySecretKeyForJWTTokenGenerationAndValidationPurposesOnly12345}")
     21    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
     22
     23    @Value("${jwt.secret}")
    1924    private String jwtSecret;
    2025
    2126    @Value("${jwt.expiration:86400000}") // 24 hours in milliseconds
    2227    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    }
    2339
    2440    private SecretKey getSigningKey() {
     
    4460
    4561    private String createToken(Map<String, Object> claims, String subject) {
     62        logger.info("🔐 Creating token - secret hash: {}", jwtSecret.hashCode());
    4663        return Jwts.builder()
    4764                .setClaims(claims)
     
    85102
    86103    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        }
    92115    }
    93116
    94117    public boolean isTokenValid(String token) {
    95118        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
    96127            Jwts.parser()
    97128                    .verifyWith(getSigningKey())
    98129                    .build()
    99130                    .parseSignedClaims(token);
     131            logger.info("✅ Token validation succeeded");
    100132            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;
    101142        } catch (Exception e) {
     143            logger.error("❌ JWT Token validation failed - {}: {}", e.getClass().getSimpleName(), e.getMessage());
    102144            return false;
    103145        }
  • backend/src/main/java/medora/util/SecurityUtil.java

    r20d16ca re1f74f6  
    11package medora.util;
    22
     3import org.springframework.stereotype.Component;
    34import jakarta.servlet.http.HttpServletRequest;
    4 import org.springframework.stereotype.Component;
    55
    66@Component
Note: See TracChangeset for help on using the changeset viewer.