Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* The main entry point for the Secure Health Backend API.
Expand All @@ -16,6 +17,7 @@
* @since 2024-01-01
*/
@SpringBootApplication
@EnableScheduling
public class SecureHealthApplication {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.beans.factory.annotation.Value;
import java.util.Arrays;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand All @@ -35,6 +37,9 @@ public class SecurityConfig {
@Autowired
private AuditLogRepository auditLogRepository;

@Value("${app.cors.allowed-origins:http://localhost:3000}")
private String allowedOrigins;

@Bean
public PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
Expand Down Expand Up @@ -103,7 +108,7 @@ private void saveSecurityLog(String action, HttpServletRequest request) {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000", "http://127.0.0.1:3000"));
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.securehealth.backend.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Value("${app.cors.allowed-origins:http://localhost:3000}")
private String allowedOrigins;

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(allowedOrigins.split(","))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
import com.securehealth.backend.dto.AppointmentDTO;
import com.securehealth.backend.dto.AppointmentRequest;
import com.securehealth.backend.model.Appointment;
import com.securehealth.backend.model.AppointmentStatus;
import com.securehealth.backend.repository.AppointmentRepository;
import com.securehealth.backend.security.PatientAccessValidator;
import com.securehealth.backend.service.AppointmentService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
Expand All @@ -34,16 +36,6 @@ public ResponseEntity<List<AppointmentDTO>> getByPatient(@PathVariable Long pati

@GetMapping("/doctor/{doctorId}")
public ResponseEntity<List<Appointment>> getByDoctor(@PathVariable Long doctorId, Authentication auth) {
// Strict Security Check: Ensure the logged-in doctor is checking their OWN schedule
String email = auth.getName();
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");

// This assumes your Appointment entity links to a Doctor Login with that ID
if (!role.equals("ADMIN") && !email.equals(appointmentRepository.findById(doctorId).map(a -> a.getDoctor().getEmail()).orElse(""))) {
// In a production app, you'd check against the DoctorProfile, but this gives the idea!
// Let's actually keep it simple: doctors can view their own schedule by their user ID.
}

return ResponseEntity.ok(appointmentRepository.findByDoctor_UserIdOrderByAppointmentDateAsc(doctorId));
}

Expand All @@ -58,7 +50,8 @@ public ResponseEntity<List<LocalTime>> getAvailableSlots(
}

@PostMapping
public ResponseEntity<?> createAppointment(@RequestBody AppointmentRequest request, Authentication auth) {
@PreAuthorize("hasAuthority('PATIENT')")
public ResponseEntity<?> createAppointment(@Valid @RequestBody AppointmentRequest request, Authentication auth) {
try {
// Extract the email from the JWT token
String email = auth.getName();
Expand All @@ -77,10 +70,12 @@ public ResponseEntity<?> createAppointment(@RequestBody AppointmentRequest reque
}

@PutMapping("/{id}/approve")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<?> approveAppointment(@PathVariable Long id, Authentication auth) {
// Enforce strict Role-Based Access Control (RBAC)
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");
if (!role.equals("ADMIN")) {
// Mock-safe security check for unit tests
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ADMIN"));
if (!isAdmin) {
return ResponseEntity.status(403).body("Forbidden: Only administrative staff can approve appointments.");
}

Expand All @@ -93,11 +88,13 @@ public ResponseEntity<?> approveAppointment(@PathVariable Long id, Authenticatio
}

@PutMapping("/{id}/reject")
public ResponseEntity<?> rejectAppointment(@PathVariable Long id, Authentication auth, @RequestBody(required = false) String reason) {
// Enforce strict Role-Based Access Control (RBAC)
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");
if (!role.equals("ADMIN")) {
return ResponseEntity.status(403).body("Forbidden: Only administrative staff can reject appointments.");
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<?> rejectAppointment(@PathVariable Long id, @RequestBody(required = false) String reason, Authentication auth) {
// Mock-safe security check for unit tests
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ADMIN"));
if (!isAdmin) {
return ResponseEntity.status(403).body("Forbidden: Only administrative staff can approve appointments.");
}

try {
Expand All @@ -109,49 +106,76 @@ public ResponseEntity<?> rejectAppointment(@PathVariable Long id, Authentication
}

@GetMapping
public ResponseEntity<?> getAllAppointments(Authentication auth) {
// Enforce RBAC: Block Patients
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");
if (role.equals("PATIENT")) {
return ResponseEntity.status(403).body("Forbidden: Patients cannot view the global clinic schedule.");
}

// Allow ADMIN and DOCTOR
@PreAuthorize("hasAnyAuthority('ADMIN', 'DOCTOR')")
public ResponseEntity<?> getAllAppointments() {
return ResponseEntity.ok(appointmentService.getAllAppointments());
}

@GetMapping("/{id}")
public ResponseEntity<?> getById(@PathVariable Long id, Authentication auth) {
return appointmentRepository.findById(id)
.map(appt -> ResponseEntity.ok((Object) appt))
.orElse(ResponseEntity.status(404).body("Appointment not found with id: " + id));
}

@GetMapping("/status/{status}")
public ResponseEntity<List<Appointment>> getByStatus(@PathVariable String status) {
return ResponseEntity.ok(appointmentRepository.findByStatus(AppointmentStatus.valueOf(status.toUpperCase())));
}

@GetMapping("/stats")
public ResponseEntity<?> getStats() {
java.util.Map<String, Object> stats = new java.util.HashMap<>();
stats.put("total", appointmentRepository.count());
stats.put("pending", appointmentRepository.countByStatus(AppointmentStatus.PENDING_APPROVAL));
stats.put("scheduled", appointmentRepository.countByStatus(AppointmentStatus.SCHEDULED));
stats.put("completed", appointmentRepository.countByStatus(AppointmentStatus.COMPLETED));
stats.put("cancelled", appointmentRepository.countByStatus(AppointmentStatus.CANCELLED));
return ResponseEntity.ok(stats);
}

@PutMapping("/{id}/complete")
@PreAuthorize("hasAuthority('DOCTOR')")
public ResponseEntity<?> completeAppointment(@PathVariable Long id, Authentication auth) {
// Enforce RBAC: Only Doctors can mark as complete
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");
if (!role.equals("DOCTOR")) {
return ResponseEntity.status(403).body("Forbidden: Only Doctors can complete appointments.");
}

try {
// auth.getName() safely gets the logged-in doctor's email from the JWT
return ResponseEntity.ok(appointmentService.completeAppointment(id, auth.getName()));
} catch (RuntimeException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}

@PutMapping("/{id}")
@PreAuthorize("hasAuthority('DOCTOR')")
public ResponseEntity<?> updateAppointment(
@PathVariable Long id,
@RequestBody com.securehealth.backend.dto.AppointmentDTO request,
Authentication auth) {

// Enforce RBAC: Only Doctors can update
String role = auth.getAuthorities().stream().findFirst().map(GrantedAuthority::getAuthority).orElse("");
if (!role.equals("DOCTOR")) {
return ResponseEntity.status(403).body("Forbidden: Only Doctors can modify appointments.");
}

try {
return ResponseEntity.ok(appointmentService.updateAppointment(id, request, auth.getName()));
} catch (RuntimeException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}

@PutMapping("/{id}/cancel")
@PreAuthorize("hasAnyAuthority('ADMIN', 'DOCTOR', 'PATIENT')")
public ResponseEntity<?> cancelAppointment(@PathVariable Long id, Authentication auth) {
try {
String role = auth.getAuthorities().stream().findFirst().get().getAuthority();
return ResponseEntity.ok(appointmentService.cancelAppointment(id, auth.getName(), role));
} catch (RuntimeException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}

@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<?> deleteAppointment(@PathVariable Long id) {
try {
appointmentService.deleteAppointment(id);
return ResponseEntity.ok("Appointment deleted successfully.");
} catch (RuntimeException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.securehealth.backend.dto.RegistrationRequest;
import com.securehealth.backend.dto.ResetPasswordRequest;
import com.securehealth.backend.model.Login;
import com.securehealth.backend.repository.LoginRepository;
import com.securehealth.backend.service.AuthService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -36,6 +37,32 @@ public class AuthController {
@Autowired
private AuthService authService;

@Autowired
private LoginRepository loginRepository;

/**
* Returns the current authenticated user's profile.
* Endpoint: GET /api/auth/me
*/
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(org.springframework.security.core.Authentication authentication) {
if (authentication == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Not authenticated"));
}
String email = authentication.getName();
return loginRepository.findByEmail(email)
.map(user -> {
Map<String, Object> profile = new HashMap<>();
profile.put("id", user.getUserId());
profile.put("email", user.getEmail());
profile.put("role", user.getRole().name());
profile.put("twoFactorEnabled", user.isTwoFactorEnabled());
profile.put("createdAt", user.getCreatedAt());
return ResponseEntity.ok(profile);
})
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "User not found")));
}

/**
* Register a new user.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.securehealth.backend.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

/**
* Controller to provide static/mock data for frontend dropdowns.
*/
@RestController
@RequestMapping("/api")
public class CatalogController {

@GetMapping("/medications")
public ResponseEntity<?> getMedications() {
return ResponseEntity.ok(List.of(
Map.of("id", 1, "name", "Amoxicillin"),
Map.of("id", 2, "name", "Lisinopril"),
Map.of("id", 3, "name", "Atorvastatin"),
Map.of("id", 4, "name", "Metformin"),
Map.of("id", 5, "name", "Ibuprofen")
));
}

@GetMapping("/test-types")
public ResponseEntity<?> getTestTypes() {
return ResponseEntity.ok(List.of(
Map.of("id", 1, "name", "Complete Blood Count (CBC)"),
Map.of("id", 2, "name", "Basic Metabolic Panel (BMP)"),
Map.of("id", 3, "name", "Lipid Panel"),
Map.of("id", 4, "name", "Urinalysis"),
Map.of("id", 5, "name", "HbA1C")
));
}

@GetMapping("/prescription-protocols")
public ResponseEntity<?> getPrescriptionProtocols() {
return ResponseEntity.ok(List.of(
Map.of("id", 1, "name", "Standard Antibiotic Protocol"),
Map.of("id", 2, "name", "Hypertension Maintenance"),
Map.of("id", 3, "name", "Diabetes Type 2 Starter")
));
}

@GetMapping("/conditions")
public ResponseEntity<?> getConditions() {
return ResponseEntity.ok(List.of(
Map.of("id", 1, "name", "Hypertension"),
Map.of("id", 2, "name", "Type 2 Diabetes"),
Map.of("id", 3, "name", "Hyperlipidemia"),
Map.of("id", 4, "name", "Asthma")
));
}

@GetMapping("/hospital-departments")
public ResponseEntity<?> getHospitalDepartments() {
return ResponseEntity.ok(List.of(
Map.of("id", 1, "name", "Cardiology"),
Map.of("id", 2, "name", "Neurology"),
Map.of("id", 3, "name", "Pediatrics"),
Map.of("id", 4, "name", "Orthopedics"),
Map.of("id", 5, "name", "General Practice")
));
}
}
Loading