Content ITV
This is Itvedant Content department
Business Scenario
In this lab, we will enhance the Medicare backend by implementing a Prescription Management Module that allows doctors to create prescriptions after completing patient appointments.
Pre-Lab Preparation
Module:
1) Mastering Springboot Security
PrescriptionRequest PrescriptionResponse MedicineDto
Final Phase 11 Flow
PATIENT books appointment ↓ DOCTOR attends appointment ↓ Doctor marks appointment COMPLETED ↓ Doctor creates prescription ↓ Diagnosis Medicines Doctor Notes ↓ Patient views prescription
Security :
DOCTOR ↓ Can prescribe only for their own appointment Appointment must be COMPLETED Only ONE prescription per appointment PATIENT ↓ Can view only their own prescriptions
Database Relationship
Appointment │ │ 1 : 0..1 ▼ Prescription │ │ 1 : Many ▼ PrescriptionMedicine
entity/ └─ Prescription.java
Create Prescription Entity
1
package com.medicare.entity;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "prescriptions")
public class Prescription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "appointment_id",nullable = false,unique = true) private Appointment appointment;
@Column(nullable = false,columnDefinition = "TEXT")
private String diagnosis;
@Column(name = "doctor_notes",columnDefinition = "TEXT")
private String doctorNotes;
@OneToMany(mappedBy = "prescription",cascade =
CascadeType.ALL,orphanRemoval = true)
private List<PrescriptionMedicine> medicines = new ArrayList<>();
@Column(name = "created_at",nullable = false,updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
public Prescription() {
}
public Long getId() {
return id;
}
public Appointment getAppointment() {
return appointment;
}
public void setAppointment(Appointment appointment) {
this.appointment = appointment;
} public String getDiagnosis() {
return diagnosis;
}
public void setDiagnosis(String diagnosis) {
this.diagnosis = diagnosis;
}
public String getDoctorNotes() {
return doctorNotes;
}
public void setDoctorNotes(String doctorNotes) {
this.doctorNotes = doctorNotes;
}
public List<PrescriptionMedicine> getMedicines() {
return medicines;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void addMedicine(PrescriptionMedicine medicine) {
medicines.add(medicine);
medicine.setPrescription(this);
}
}Why unique = true?
Appointment 10 ├── Prescription 5 └── Prescription 8
DTO #1 — MedicineDto
2
dto/ └─ MedicineDto.java
package com.medicare.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class MedicineDto {
@NotBlank(message = "Medicine name is required")
@Size(max = 150)
private String medicineName;
@NotBlank(message = "Dosage is required")
@Size(max = 100)
private String dosage;
@NotBlank(message = "Frequency is required")
@Size(max = 100)
private String frequency;
@NotBlank(message = "Duration is required")
@Size(max = 100)
private String duration;
public MedicineDto() {
} public MedicineDto(
String medicineName,
String dosage,
String frequency,
String duration) {
this.medicineName = medicineName;
this.dosage = dosage;
this.frequency = frequency;
this.duration = duration;
}
public String getMedicineName() {
return medicineName;
}
public void setMedicineName(
String medicineName) {
this.medicineName = medicineName;
}
public String getDosage() {
return dosage;
}
public void setDosage(
String dosage) {
this.dosage = dosage;
} public String getFrequency() {
return frequency;
}
public void setFrequency(
String frequency) {
this.frequency = frequency;
}
public String getDuration() {
return duration;
}
public void setDuration(
String duration) {
this.duration = duration;
}
}This one DTO is used for both:
Prescription Request Prescription Response
DTO #2 — PrescriptionRequest
3
dto/ └─ PrescriptionRequest.java
package com.medicare.dto;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class PrescriptionRequest {
@NotNull(message = "Appointment id is required")
private Long appointmentId;
@NotBlank(message = "Diagnosis is required")
@Size(max = 2000,message = "Diagnosis is too long")
private String diagnosis; @Size(max = 3000,message = "Doctor notes are too long")
private String doctorNotes;
@NotEmpty(message = "At least one medicine is required")
@Valid
private List<MedicineDto> medicines;
public Long getAppointmentId() {
return appointmentId;
}
public void setAppointmentId(
Long appointmentId) {
this.appointmentId = appointmentId;
}
public String getDiagnosis() {
return diagnosis;
}
public void setDiagnosis(String diagnosis) {
this.diagnosis = diagnosis;
}
public String getDoctorNotes() {
return doctorNotes;
} public void setDoctorNotes(
String doctorNotes) {
this.doctorNotes = doctorNotes;
}
public List<MedicineDto> getMedicines() {
return medicines;
}
public void setMedicines(
List<MedicineDto> medicines) {
this.medicines = medicines;
}
}DTO #3 — PrescriptionResponse
4
dto/ └─PrescriptionResponse.java
package com.medicare.dto;import java.time.LocalDateTime;
import java.util.List;
public class PrescriptionResponse {
private Long id;
private Long appointmentId;
private Long doctorId;
private String doctorName;
private Long patientId;
private String patientName;
private String diagnosis;
private String doctorNotes;
private List<MedicineDto> medicines;
private LocalDateTime createdAt;
public PrescriptionResponse(
Long id,
Long appointmentId,
Long doctorId,
String doctorName,
Long patientId,
String patientName,
String diagnosis,
String doctorNotes,
List<MedicineDto> medicines,
LocalDateTime createdAt) {
this.id = id;
this.appointmentId = appointmentId;
this.doctorId = doctorId; this.doctorName = doctorName;
this.patientId = patientId;
this.patientName = patientName;
this.diagnosis = diagnosis;
this.doctorNotes = doctorNotes;
this.medicines = medicines;
this.createdAt = createdAt;
}
public Long getId() {
return id;
}
public Long getAppointmentId() {
return appointmentId;
}
public Long getDoctorId() {
return doctorId;
}
public String getDoctorName() {
return doctorName;
}
public Long getPatientId() {
return patientId;
} public String getPatientName() {
return patientName;
}
public String getDiagnosis() {
return diagnosis;
}
public String getDoctorNotes() {
return doctorNotes;
}
public List<MedicineDto> getMedicines() {
return medicines;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}Create PrescriptionRepository
5
repository/ └─PrescriptionRepository.java
package com.medicare.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.Prescription;
public interface PrescriptionRepository extends JpaRepository<Prescription, Long> {
boolean existsByAppointmentId(Long appointmentId);
Optional<Prescription> findByAppointmentId(Long appointmentId);
List<Prescription> findByAppointment_Patient_Id(
Long patientId
);
}Do We Need PrescriptionMedicineRepository?
Create PrescriptionNotFoundException
6
exception/ └─PrescriptionNotFoundException.java
package com.medicare.exception;
public class PrescriptionNotFoundException extends RuntimeException {
public PrescriptionNotFoundException(String message) {
super(message);
}
}Create PrescriptionConflictException
7
exception/ └─PrescriptionConflictException.java
package com.medicare.exception;
public class PrescriptionConflictException
extends RuntimeException {
public PrescriptionConflictException(
String message) {
super(message);
}
}Create PrescriptionService
8
service/ └─PrescriptionService.java
package com.medicare.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.medicare.dto.MedicineDto;
import com.medicare.dto.PrescriptionRequest;
import com.medicare.dto.PrescriptionResponse;
import com.medicare.entity.Appointment;
import com.medicare.entity.AppointmentStatus;
import com.medicare.entity.DoctorProfile;
import com.medicare.entity.Prescription;
import com.medicare.entity.PrescriptionMedicine;import com.medicare.entity.Role;
import com.medicare.entity.User;
import com.medicare.exception.AppointmentNotFoundException;
import com.medicare.exception.DoctorNotFoundException;
import com.medicare.exception.PrescriptionConflictException;
import com.medicare.exception.PrescriptionNotFoundException;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.AppointmentRepository;
import com.medicare.repository.DoctorProfileRepository;
import com.medicare.repository.PrescriptionRepository;
import com.medicare.repository.UserRepository;
@Service
public class PrescriptionService {
private final PrescriptionRepository prescriptionRepository;
private final AppointmentRepository appointmentRepository;
private final UserRepository userRepository;
private final DoctorProfileRepository doctorRepository;
public PrescriptionService(
PrescriptionRepository prescriptionRepository,
AppointmentRepository appointmentRepository,
UserRepository userRepository,
DoctorProfileRepository doctorRepository) {
this.prescriptionRepository = prescriptionRepository;
this.appointmentRepository = appointmentRepository;
this.userRepository = userRepository;
this.doctorRepository = doctorRepository;
} // DOCTOR - CREATE PRESCRIPTION
@Transactional
@PreAuthorize("hasRole('DOCTOR')")
public PrescriptionResponse createPrescription(
PrescriptionRequest request, String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId());
Appointment appointment = findAppointmentById(
request.getAppointmentId());
// Only the assigned doctor can create the prescription.
if (!appointment.getDoctor().getId().equals(doctor.getId())) {
throw new AccessDeniedException(
"You cannot create a prescription for this appointment");
}
// Prescription can only be created after consultation is completed.
if (appointment.getAppointmentStatus() != AppointmentStatus.COMPLETED) {
throw new IllegalArgumentException(
"Prescription can only be created for a completed appointment");
}
// Only one prescription is allowed per appointment.
if (prescriptionRepository.existsByAppointmentId(appointment.getId())) {
throw new PrescriptionConflictException(
"Prescription already exists for this appointment");
}
Prescription prescription = new Prescription();
prescription.setAppointment(appointment); prescription.setDiagnosis(request.getDiagnosis());
prescription.setDoctorNotes(request.getDoctorNotes());
for (MedicineDto medicineDto : request.getMedicines()) {
PrescriptionMedicine medicine = new PrescriptionMedicine();
medicine.setMedicineName(medicineDto.getMedicineName());
medicine.setDosage(medicineDto.getDosage());
medicine.setFrequency(medicineDto.getFrequency());
medicine.setDuration(medicineDto.getDuration());
prescription.addMedicine(medicine);
}
Prescription savedPrescription = prescriptionRepository
.save(prescription);
return mapToPrescriptionResponse(savedPrescription);
}
// VIEW PRESCRIPTION FOR APPOINTMENT
@Transactional(readOnly = true)
@PreAuthorize("hasAnyRole('PATIENT', 'DOCTOR')")
public PrescriptionResponse getPrescriptionByAppointment(
Long appointmentId, String email) {
Prescription prescription = prescriptionRepository
.findByAppointmentId(appointmentId)
.orElseThrow(() -> new PrescriptionNotFoundException(
"Prescription not found for appointment id: "
+ appointmentId));
User user = findUserByEmail(email);
verifyPrescriptionAccess(prescription, user); return mapToPrescriptionResponse(prescription);
}
// PATIENT - VIEW OWN PRESCRIPTIONS
@Transactional(readOnly = true)
@PreAuthorize("hasRole('PATIENT')")
public List<PrescriptionResponse> getMyPrescriptions(String email) {
User patient = findUserByEmail(email);
List<Prescription> prescriptions =
prescriptionRepository.findByAppointment_Patient_Id(
patient.getId());
List<PrescriptionResponse> responses = new ArrayList<>();
for (Prescription prescription : prescriptions) {
responses.add(mapToPrescriptionResponse(prescription));
}
return responses;
}
// OWNERSHIP CHECK
private void verifyPrescriptionAccess(
Prescription prescription, User user) {
Appointment appointment = prescription.getAppointment();
if (user.getRole() == Role.PATIENT) {
if (!appointment.getPatient().getId().equals(user.getId())) {
throw new AccessDeniedException("
You cannot access this prescription");
} return;
}
if (user.getRole() == Role.DOCTOR) {
DoctorProfile doctor = findDoctorByUserId(user.getId());
if (!appointment.getDoctor().getId().equals(doctor.getId())) {
throw new AccessDeniedException(
"You cannot access this prescription");
}
}
}
// HELPER METHODS
private User findUserByEmail(String email) {
return userRepository.findByEmailIgnoreCase(email)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
private DoctorProfile findDoctorByUserId(Long userId) {
return doctorRepository.findByUserId(userId)
.orElseThrow(() ->
new DoctorNotFoundException("Doctor profile not found"));
}
private Appointment findAppointmentById(Long appointmentId) {
return appointmentRepository.findById(appointmentId)
.orElseThrow(() -> new AppointmentNotFoundException(
"Appointment not found with id: " + appointmentId));
} // RESPONSE MAPPING
private PrescriptionResponse mapToPrescriptionResponse(
Prescription prescription) {
List<MedicineDto> medicines = new ArrayList<>();
for (PrescriptionMedicine medicine : prescription.getMedicines()) {
MedicineDto medicineDto = new MedicineDto(
medicine.getMedicineName(),
medicine.getDosage(),
medicine.getFrequency(),
medicine.getDuration());
medicines.add(medicineDto);
}
Appointment appointment = prescription.getAppointment();
return new PrescriptionResponse(
prescription.getId(),
appointment.getId(),
appointment.getDoctor().getId(),
appointment.getDoctor().getUser().getName(),
appointment.getPatient().getId(),
appointment.getPatient().getName(),
prescription.getDiagnosis(),
prescription.getDoctorNotes(),
medicines, prescription.getCreatedAt());
}
}Why @Transactional?
Prescription + Medicine 1 + Medicine 2 + Medicine 3
BEGIN save prescription save medicine 1 save medicine 2 save medicine 3 SUCCESS ↓ COMMIT
public void setTimeSlot(
TimeSlot timeSlot) {
this.timeSlot = timeSlot;
}
}How Do We Know the Patient Now ?
JWT ↓ email ↓ SecurityContextHolder ↓ Authentication
Enable Method Security
3
import org.springframework.security.config.annotation.method.configuration.
EnableMethodSecurity;Now we can use: @PreAuthorize(...)
@Configuration
@EnableMethodSecurity
public class SecurityConfig {Understanding hasRole()
PATIENT ↓ ROLE_PATIENT
DOCTOR ↓ ROLE_DOCTOR
Update DoctorProfileRepository
4
Optional<DoctorProfile> findByUserId( Long userId );boolean existsByUserId(Long userId);
Optional<DoctorProfile> findByUserId(
Long userId
);import java.util.Optional;Secure Doctor Profile Ownership
5
JWT ↓ Current Doctor User ↓ Their DoctorProfile
package com.medicare.dto;
import java.math.BigDecimal;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
public class DoctorProfileRequest {
@NotBlank(message = "Specialization is required")
@Size(max = 100)
private String specialization;
@NotNull(message = "Experience is required")
@Min(0)
@Max(60)
private Integer experienceYears;
@NotNull(message = "Consultation fee is required")
@Positive
private BigDecimal consultationFee;
@Size(max = 500)
private String photoUrl;
public String getSpecialization() {
return specialization;
}
public void setSpecialization(
String specialization) {
this.specialization = specialization;
} public Integer getExperienceYears() {
return experienceYears;
}
public void setExperienceYears(
Integer experienceYears) {
this.experienceYears = experienceYears;
}
public BigDecimal getConsultationFee() {
return consultationFee;
}
public void setConsultationFee(
BigDecimal consultationFee) {
this.consultationFee = consultationFee;
}
public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(
String photoUrl) {
this.photoUrl = photoUrl;
}
}Doctor Profile Endpoints Change
6
PUT /api/doctors/{doctorId}
DELETE /api/doctors/{doctorId}for profile management.
POST /api/doctors/profile GET /api/doctors/profile/me PUT /api/doctors/profile/me DELETE /api/doctors/profile/me
Update DoctorService
7
@PreAuthorize("hasRole('DOCTOR')")
public DoctorResponse createMyProfile(DoctorProfileRequest request,String email) {
User user = findUserByEmail(email);
if (doctorRepository.existsByUserId(user.getId())) {
throw new IllegalArgumentException( "Doctor profile already exists");
}
DoctorProfile doctor = new DoctorProfile();
doctor.setUser(user);
doctor.setSpecialization(request.getSpecialization());
doctor.setExperienceYears(request.getExperienceYears());
doctor.setConsultationFee(request.getConsultationFee()); doctor.setPhotoUrl(request.getPhotoUrl());
return mapToDoctorResponse(doctorRepository.save(doctor));
}@PreAuthorize("hasRole('DOCTOR')")
public DoctorResponse getMyProfile(String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId());
return mapToDoctorResponse(doctor);
}@PreAuthorize("hasRole('DOCTOR')")
public DoctorResponse updateMyProfile(String email,DoctorProfileRequest request) {
User user = findUserByEmail(email); DoctorProfile doctor = findDoctorByUserId(user.getId());
doctor.setSpecialization(request.getSpecialization());
doctor.setExperienceYears(request.getExperienceYears());
doctor.setConsultationFee(request.getConsultationFee());
doctor.setPhotoUrl(request.getPhotoUrl());
return mapToDoctorResponse(doctorRepository.save(doctor));
}@PreAuthorize("hasRole('DOCTOR')")
public void deleteMyProfile(String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId());
doctorRepository.delete(doctor);
}Add Helper Methods
8
private User findUserByEmail(String email) {
return userRepository.findByEmailIgnoreCase(email).orElseThrow(() ->
new UserNotFoundException("User not found")
);
}private DoctorProfile findDoctorByUserId(Long userId) {
return doctorRepository.findByUserId(userId).orElseThrow(() ->
new DoctorNotFoundException("Doctor profile not found")
);
}Before proceeding to the next steps, we need to clean up the existing DoctorService code by removing outdated methods
Update DoctorController
9
@PostMapping("/profile")
public ResponseEntity<DoctorResponse>createMyProfile(
@Valid @RequestBodyDoctorProfileRequest request, Authentication authentication) {
DoctorResponse response = doctorService.createMyProfile(request,
authentication.getName());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}@GetMapping("/profile/me")
public ResponseEntity<DoctorResponse>getMyProfile(Authentication authentication) {
return ResponseEntity.ok(doctorService.getMyProfile(authentication.getName())
);
}@PutMapping("/profile/me")
public ResponseEntity<DoctorResponse>updateMyProfile(
@Valid @RequestBody DoctorProfileRequest request, Authentication authentication) {
return ResponseEntity.ok(doctorService.updateMyProfile(
authentication.getName(),request)
);
}@DeleteMapping("/profile/me")
public ResponseEntity<Void> deleteMyProfile(Authentication authentication) {
doctorService.deleteMyProfile(authentication.getName());
return ResponseEntity.noContent().build();
}import org.springframework.security.core.Authentication;Fix Appointment Slot Query
10
existsByDoctorIdAndAppointmentDateAndTimeSlot(...)
boolean existsByDoctorIdAndAppointmentDateAndTimeSlotAndAppointmentStatusNot(
Long doctorId,
LocalDate appointmentDate,
TimeSlot timeSlot,
AppointmentStatus appointmentStatus
);Refactor AppointmentService
11
@PreAuthorize("hasRole('PATIENT')")
public AppointmentResponse createAppointment(AppointmentRequest request,
String email) {
User patient = findUserByEmail(email);
DoctorProfile doctor = doctorRepository.findById(request.getDoctorId())
.orElseThrow(() -> new DoctorNotFoundException(
"Doctor not found with id: "+ request.getDoctorId()
)
);
boolean booked = appointmentRepository
.existsByDoctorIdAndAppointmentDateAndTimeSlotAndAppointmentStatusNot(
doctor.getId(),request.getAppointmentDate(),request.getTimeSlot(),
AppointmentStatus.CANCELLED); if (booked) {
throw new AppointmentConflictException(
"Selected appointment slot is already booked");
}
Appointment appointment = new Appointment();
appointment.setPatient(patient);
appointment.setDoctor(doctor);
appointment.setAppointmentDate(request.getAppointmentDate());
appointment.setTimeSlot(request.getTimeSlot());
appointment.setConsultationFee(doctor.getConsultationFee());
appointment.setAppointmentStatus(AppointmentStatus.PENDING);
appointment.setPaymentStatus(PaymentStatus.UNPAID);
Appointment savedAppointment = appointmentRepository.save(appointment);
return mapToAppointmentResponse(savedAppointment);
}Patient — My Appointments
12
@PreAuthorize("hasRole('PATIENT')")
public List<AppointmentResponse>
getMyAppointments(String email) { User patient = findUserByEmail(email);
List<Appointment> appointments = appointmentRepository
.findByPatientId(patient.getId());
return mapAppointmentList(appointments);
}Doctor — My Schedule
13
@PreAuthorize("hasRole('DOCTOR')")
public List<AppointmentResponse>
getMyDoctorAppointments(
String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId()); List<Appointment> appointments = appointmentRepository
.findByDoctorId(doctor.getId());
return mapAppointmentList(appointments);
}Secure Appointment by ID
14
@PreAuthorize("hasAnyRole('PATIENT', 'DOCTOR')")
public AppointmentResponse getAppointmentById( Long appointmentId,String email) {
Appointment appointment = findAppointmentById(appointmentId);
User user = findUserByEmail(email);
verifyAppointmentAccess(appointment,user);
return mapToAppointmentResponse(appointment);
}Ownership Check
15
private void verifyAppointmentAccess(Appointment appointment,User user) {
if (user.getRole() == Role.PATIENT) {
if (!appointment.getPatient().getId().equals(user.getId())) {
throw new AccessDeniedException( "You cannot access this appointment"); }
return;
}
if (user.getRole() == Role.DOCTOR) {
DoctorProfile doctor = findDoctorByUserId(user.getId());
if (!appointment.getDoctor().getId().equals(doctor.getId())) {
throw new AccessDeniedException("You cannot access this appointment");
}
}
}import org.springframework.security.access.AccessDeniedException;Patient Cancel Appointment
16
@PreAuthorize("hasRole('PATIENT')")
public AppointmentResponse cancelAppointment(Long appointmentId,String email) {
User patient = findUserByEmail(email);
Appointment appointment = findAppointmentById(appointmentId);
if (!appointment.getPatient().getId().equals(patient.getId())) {
throw new AccessDeniedException("You cannot cancel this appointment");
}
if (appointment.getAppointmentStatus()== AppointmentStatus.COMPLETED) {
throw new IllegalArgumentException(
"Completed appointment cannot be cancelled");
}
if (appointment.getAppointmentStatus() == AppointmentStatus.CANCELLED) {
throw new IllegalArgumentException("Appointment is already cancelled");
}
appointment.setAppointmentStatus(AppointmentStatus.CANCELLED);
return mapToAppointmentResponse(appointmentRepository.save(appointment));
}Doctor Complete Appointment
17
@PreAuthorize("hasRole('DOCTOR')")
public AppointmentResponse completeAppointment(Long appointmentId,String email) {
User user = findUserByEmail(email);
DoctorProfile doctor = findDoctorByUserId(user.getId());
Appointment appointment = findAppointmentById(appointmentId);
if (!appointment.getDoctor().getId().equals(doctor.getId())) {
throw new AccessDeniedException("You cannot complete this appointment");
}
if (appointment.getAppointmentStatus() == AppointmentStatus.CANCELLED) {
throw new IllegalArgumentException("Cancelled appointment cannot be completed");
}
appointment.setAppointmentStatus(AppointmentStatus.COMPLETED);
return mapToAppointmentResponse(appointmentRepository.save(appointment)
);
} return mapToAppointmentResponse(appointmentRepository.save(appointment)
);
}Appointment Mapping
18
private AppointmentResponse mapToAppointmentResponse(Appointment appointment) {
return new AppointmentResponse(appointment
.getId(),appointment.getPatient().getId(),
appointment.getPatient().getName(),
appointment.getDoctor().getId(),
appointment.getDoctor().getUser().getName(),
appointment.getDoctor().getSpecialization(),
appointment.getAppointmentDate(),
appointment.getTimeSlot(),
appointment.getConsultationFee(),
appointment.getAppointmentStatus(),
appointment.getPaymentStatus(), appointment.getCreatedAt()
);
}private List<AppointmentResponse>mapAppointmentList(
List<Appointment> appointments) {
List<AppointmentResponse> responses = new ArrayList<>();
for (Appointment appointment : appointments) {
responses.add(mapToAppointmentResponse(appointment));
}
return responses;
}Update AppointmentController
19
@PostMapping
public ResponseEntity<AppointmentResponse>
createAppointment(@Valid @RequestBody AppointmentRequest request,
Authentication authentication) {
AppointmentResponse response = appointmentService.createAppointment(
request,authentication.getName()
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}@GetMapping("/my")
public ResponseEntity<List<AppointmentResponse>>
getMyAppointments(Authentication authentication) {
return ResponseEntity.ok(appointmentService.getMyAppointments( authentication.getName()
)
);
}@GetMapping("/doctor/me")
public ResponseEntity<List<AppointmentResponse>>
getMyDoctorAppointments(Authentication authentication) {
return ResponseEntity.ok(appointmentService.getMyDoctorAppointments(
authentication.getName()
)
);
}@GetMapping("/{id}")public ResponseEntity<AppointmentResponse>
getAppointmentById( @PathVariable Long id,Authentication authentication) {
return ResponseEntity.ok(appointmentService.getAppointmentById(
id,authentication.getName()
)
);
}@PatchMapping("/{id}/cancel")
public ResponseEntity<AppointmentResponse>
cancelAppointment(@PathVariable Long id, Authentication authentication) {
return ResponseEntity.ok(appointmentService.cancelAppointment(
id,authentication.getName()
)
);
}@PatchMapping("/{id}/complete")
public ResponseEntity<AppointmentResponse>
completeAppointment(@PathVariable Long id,Authentication authentication) {
return ResponseEntity.ok(appointmentService.completeAppointment(
id,authentication.getName()
)
);
}Remove Insecure Appointment Endpoints
20
GET /api/appointments GET /api/appointments/patient/{patientId} GET /api/appointments/doctor/{doctorId}
PATIENT ↓ /appointments/my
DOCTOR ↓ /appointments/doctor/me
Remove Insecure Appointment Endpoints
21
public UserResponse getCurrentUser(String email) {
User user = userRepository.findByEmailIgnoreCase(email).orElseThrow(() ->
new UserNotFoundException("User not found")
);
return mapToUserResponse(user);
}@GetMapping("/me")
public ResponseEntity<UserResponse>getCurrentUser(Authentication authentication) { return ResponseEntity.ok(userService.getCurrentUser(authentication.getName())
);
}{
"id": 1,
"name": "Rahul Shah",
"email": "rahul@example.com",
"phone": "9876543210",
"role": "PATIENT"
}GET /api/users
GET /api/users/{id}Configure Endpoint-Level RBAC
22
from the controller for now.
.authorizeHttpRequests(auth -> auth
// Public APIs
.requestMatchers("/api/health","/api/auth/register","/api/auth/login")
.permitAll()
// Current user's profile
.requestMatchers("/api/users/me").authenticated()
// Doctor can manage only own profile .requestMatchers("/api/doctors/profile","/api/doctors/profile/**")
.hasRole("DOCTOR")
// Doctor search/listing
.requestMatchers(HttpMethod.GET,"/api/doctors/**")
.hasAnyRole("PATIENT", "DOCTOR")
// Patient books appointments
.requestMatchers(HttpMethod.POST,"/api/appointments")
.hasRole("PATIENT")
// Patient appointment history
.requestMatchers("/api/appointments/my").hasRole("PATIENT")
// Doctor schedule
.requestMatchers("/api/appointments/doctor/**").hasRole("DOCTOR")
// Patient cancellation
.requestMatchers(HttpMethod.PATCH,"/api/appointments/*/cancel")
.hasRole("PATIENT")
// Doctor completion
.requestMatchers(HttpMethod.PATCH,"/api/appointments/*/complete")
.hasRole("DOCTOR")
// Appointment details
.requestMatchers(HttpMethod.GET,"/api/appointments/*")
.hasAnyRole("PATIENT", "DOCTOR")
/* * Safer default: Anything we forgot to configure is denied.
*/
.anyRequest()
.denyAll()
)
Why Both SecurityConfig AND @PreAuthorize?
Yes — intentionally for sensitive business actions.
HTTP endpoint:
POST /appointments
↓
SecurityConfig:
PATIENT required
↓
Controller
↓
Service:
@PreAuthorize("hasRole('PATIENT')")Proper 401 and 403 JSON Responses
23
security/ └── CustomAuthenticationEntryPoint.java
package com.medicare.security;
import java.io.IOException;
import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.medicare.response.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tools.jackson.databind.json.JsonMapper;
@Component
public class CustomAuthenticationEntryPoint
implements AuthenticationEntryPoint {
private final JsonMapper jsonMapper;
public CustomAuthenticationEntryPoint(
JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException {
ErrorResponse errorResponse =
new ErrorResponse(
LocalDateTime.now(),
HttpStatus.UNAUTHORIZED.value(),
"Authentication is required",
request.getRequestURI()
);
response.setStatus(
HttpStatus.UNAUTHORIZED.value()
);
response.setContentType(
MediaType.APPLICATION_JSON_VALUE
);
response.getWriter().write(
jsonMapper.writeValueAsString(
errorResponse
)
);
}
}import java.io.IOException;
import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.medicare.response.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tools.jackson.databind.json.JsonMapper;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final JsonMapper jsonMapper;
public CustomAuthenticationEntryPoint(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
@Override
public void commence(HttpServletRequest request,HttpServletResponse response,
AuthenticationException exception) throws IOException {
ErrorResponse errorResponse = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.UNAUTHORIZED.value(),
"Authentication is required",
request.getRequestURI()
); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(jsonMapper.writeValueAsString(errorResponse));
}
}Create CustomAccessDeniedHandler
24
security/ └── CustomAccessDeniedHandler.java
package com.medicare.security;
import java.io.IOException;
import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import com.medicare.response.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tools.jackson.databind.json.JsonMapper;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final JsonMapper jsonMapper;
public CustomAccessDeniedHandler(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
@Override
public void handle(
HttpServletRequest request,HttpServletResponse response,
AccessDeniedException exception) throws IOException {
ErrorResponse errorResponse = new ErrorResponse(
LocalDateTime.now(), HttpStatus.FORBIDDEN.value(),
"You do not have permission to access this resource",
request.getRequestURI()
);
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(jsonMapper.writeValueAsString(errorResponse)
);
}
}Create CustomAccessDeniedHandler
25
.exceptionHandling(exception ->
exception.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)Not authenticated - 401 UNAUTHORIZED Authenticated but wrong role - 403 FORBIDDEN
Important Difference — Role vs Ownership
Is Rahul a PATIENT? --> YESDoes not mean: Rahul can view every patient appointmentWe still need: Does this appointment belong to Rahul?
Authentication ↓ Role ↓ Ownership
Testing — Patient Books Appointment
26
Login as Rahul: ROLE_PATIENT
POST /api/appointments Authorization: Bearer <patient-token>
{
"doctorId": 1,
"appointmentDate": "2026-09-25",
"timeSlot": "MORNING_10_AM"
}patientId from the frontend because the backend automatically identifies the logged-in patient, Rahul, using the JWT token.Doctor Tries to Book Appointment
27
Because: DOCTOR ≠ PATIENT
Patient — My Appointments
28
GET /api/appointments/my Authorization: Bearer <patient-token>
Doctor Tries /appointments/my
29
Doctor — Own Schedule
30
GET /api/appointments/doctor/me Authorization: Bearer <doctor-token>
Patient Tries Doctor Schedule
31
GET /api/appointments/doctor/me
Test Patient Ownership
32
Rahul owns Appointment 5 Priya owns Appointment 6
GET /api/appointments/5
GET /api/appointments/6
Test Doctor Ownership
33
Appointment 5 belongs to: Dr. Ananya
GET /api/appointments/5
GET /api/appointments/5
Patient Cancel Own Appointment
34
PATCH /api/appointments/5/cancel
Another Patient Tries to Cancel It
35
PATCH /api/appointments/5/complete
Doctor Completes Own Appointment
36
Patient Tries Complete
37
Doctor Profile Security
38
POST /api/doctors/profile
{
"specialization": "Cardiologist",
"experienceYears": 8,
"consultationFee": 1200,
"photoUrl": "..."
}Patient Tries Creating Doctor Profile
39
We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully implemented Role-Based Access Control (RBAC) and ownership security in the Medicare backend using Spring Security and JWT authentication.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) Mastering Springboot Security
By Content ITV