patient see their multiple studies

This commit is contained in:
mario
2025-05-13 09:52:45 +07:00
parent 13bb380f51
commit 2d1f135fda
7 changed files with 193 additions and 58 deletions

View File

@@ -61,11 +61,67 @@ func (h *DicomHandler) ForwardRequest(w http.ResponseWriter, r *http.Request) {
if claims != nil {
switch claims.Role {
case "patient":
// For patients requesting study list, filter to only show their study
// For patients requesting study list, filter to only show their studies
if strings.HasPrefix(urlPath, "/studies") && !strings.Contains(urlPath, "/") {
// Ensure only the patient's study is shown
// Check if multiple studies are available in the claim
if len(claims.StudyIUIDs) > 0 {
// Remove existing StudyInstanceUID param if it exists
queryParams.Del("StudyInstanceUID")
// For DICOMweb, we can use comma-separated UIDs
queryParams.Set("StudyInstanceUID", strings.Join(claims.StudyIUIDs, ","))
h.logger.Debug("Filtering by multiple studies",
zap.Strings("studies", claims.StudyIUIDs),
)
} else if claims.StudyIUID != "" {
// Fallback for backward compatibility - ensure only the patient's study is shown
queryParams.Set("StudyInstanceUID", claims.StudyIUID)
}
} else if strings.HasPrefix(urlPath, "/studies/") {
// This is a request for a specific study - check if the patient is authorized
// Extract the study ID from the path
// Format: /studies/{studyID}/...
pathParts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
if len(pathParts) >= 2 {
studyID := pathParts[1]
// Check if this study is in the patient's authorized studies
authorized := false
// First check StudyIUIDs array
if len(claims.StudyIUIDs) > 0 {
for _, id := range claims.StudyIUIDs {
if id == studyID {
authorized = true
break
}
}
}
// Then check single StudyIUID for backward compatibility
if !authorized && claims.StudyIUID == studyID {
authorized = true
}
// If not authorized, return 403 Forbidden
if !authorized {
h.logger.Warn("Unauthorized study access attempt",
zap.String("studyID", studyID),
zap.String("patientID", claims.PatientID),
zap.Strings("authorizedStudies", claims.StudyIUIDs),
)
http.Error(w, "Forbidden: You are not authorized to access this study", http.StatusForbidden)
return
}
h.logger.Debug("Authorized access to specific study",
zap.String("studyID", studyID),
zap.String("patientID", claims.PatientID),
)
}
}
case "ref_doctor":
// For ref_doctor requesting study list, apply filter from token
if strings.HasPrefix(urlPath, "/studies") && !strings.Contains(urlPath, "/") && claims.FilterURL != "" {

View File

@@ -187,12 +187,39 @@ func PatientViewRestriction(logger *zap.Logger) func(http.Handler) http.Handler
}
// If a study is being requested, verify patient has access
if requestedStudyUID != "" && requestedStudyUID != claims.StudyIUID {
if requestedStudyUID != "" {
// Check if the requested study is authorized
isAuthorized := false
// First check the array of studies if available
if len(claims.StudyIUIDs) > 0 {
for _, studyUID := range claims.StudyIUIDs {
if studyUID == requestedStudyUID {
isAuthorized = true
logger.Debug("Patient authorized to access study from StudyIUIDs array",
zap.String("userID", claims.UserID),
zap.String("requestedStudy", requestedStudyUID))
break
}
}
}
// If not found in the array, check the single StudyIUID for backward compatibility
if !isAuthorized && claims.StudyIUID == requestedStudyUID {
isAuthorized = true
logger.Debug("Patient authorized to access study from StudyIUID",
zap.String("userID", claims.UserID),
zap.String("requestedStudy", requestedStudyUID))
}
// If still not authorized, return 403 Forbidden
if !isAuthorized {
logger.Warn("Patient attempted to access unauthorized study",
zap.String("userID", claims.UserID),
zap.String("role", claims.Role),
zap.String("authorizedStudy", claims.StudyIUID),
zap.String("requestedStudy", requestedStudyUID))
zap.String("requestedStudy", requestedStudyUID),
zap.Strings("authorizedStudies", claims.StudyIUIDs),
zap.String("authorizedStudy", claims.StudyIUID))
// Return 403 Forbidden with a clear message
w.Header().Set("Content-Type", "application/json")
@@ -203,6 +230,7 @@ func PatientViewRestriction(logger *zap.Logger) func(http.Handler) http.Handler
})
return
}
}
// Patient has access or is requesting a list (which will be filtered)
next.ServeHTTP(w, r)

View File

@@ -40,8 +40,10 @@ var MockUsers = []User{
type PatientData struct {
PatientID string `json:"patient_id"`
UserID string `json:"user_id"`
StudyIUID string `json:"study_iuid"`
AccessionNumber string `json:"accession_number"`
StudyIUID string `json:"study_iuid,omitempty"` // For backward compatibility
StudyIUIDs []string `json:"study_iuids,omitempty"` // Multiple study IDs
AccessionNumber string `json:"accession_number,omitempty"` // For backward compatibility
AccessionNumbers []string `json:"accession_numbers,omitempty"` // Multiple accession numbers
PatientName string `json:"patient_name"`
ReferringPhysician string `json:"referring_physician"`
}
@@ -51,8 +53,8 @@ var MockPatients = []PatientData{
{
PatientID: "00211622",
UserID: "2",
StudyIUID: "1.2.826.0.1.3680043.9.7307.1.20180713036",
AccessionNumber: "CR.180713.036",
StudyIUIDs: []string{"1.2.826.0.1.3680043.9.7307.1.20180530066", "1.2.826.0.1.3680043.9.7307.1.20180713036"},
AccessionNumbers: []string{"CR.180530.066", "CR.180713.036"},
PatientName: "DIDIT SUYATNA^R.10049.18",
ReferringPhysician: "DR. HERWINDO RIDWAN, SP.OT",
},

View File

@@ -25,8 +25,10 @@ type RefreshToken struct {
type PatientDetails struct {
PatientID string `json:"patient_id"`
PatientName string `json:"patient_name"`
AccessionNumber string `json:"accession_number"`
StudyInstanceUID string `json:"study_instance_uid"`
AccessionNumber string `json:"accession_number,omitempty"` // For backward compatibility
AccessionNumbers []string `json:"accession_numbers,omitempty"` // Multiple accession numbers
StudyInstanceUID string `json:"study_instance_uid,omitempty"` // For backward compatibility
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"` // Multiple study IDs
}
// DoctorDetails contains doctor-specific data

View File

@@ -115,7 +115,7 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
})
})
// Add this to your routes setup - in the public routes group
// * DEBUG PURPOSE ONLY *
r.Get("/debug/token", func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {

View File

@@ -39,7 +39,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
}
// Create token claims based on user role
additionalClaims := make(map[string]string)
additionalClaims := make(map[string]interface{})
var redirectURL string
switch user.Role {
@@ -53,12 +53,29 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
// Set patient-specific claims
additionalClaims["patient_id"] = patientData.PatientID
additionalClaims["patient_name"] = patientData.PatientName
additionalClaims["accession_number"] = patientData.AccessionNumber
additionalClaims["study_iuid"] = patientData.StudyIUID
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUID)
additionalClaims["study_list"] = "disabled"
// Handle multiple studies if available
if len(patientData.StudyIUIDs) > 0 {
// Store all studies in the token for DICOMWeb access
additionalClaims["study_iuids"] = patientData.StudyIUIDs
additionalClaims["accession_numbers"] = patientData.AccessionNumbers
// Set the first study as default for display and backward compatibility
additionalClaims["study_iuid"] = patientData.StudyIUIDs[0]
additionalClaims["accession_number"] = patientData.AccessionNumbers[0]
// Only use the first study in the redirect URL
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
} else {
// Fall back to single study for backward compatibility
additionalClaims["study_iuid"] = patientData.StudyIUID
additionalClaims["accession_number"] = patientData.AccessionNumber
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUID)
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUID)
}
additionalClaims["study_list"] = "disabled"
case "ref_doctor":
// Set referring doctor claims
@@ -112,7 +129,9 @@ func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
}
// Build additionalClaims from the refresh token
additionalClaims := make(map[string]string)
additionalClaims := make(map[string]interface{})
// Add string claims
if claims.PatientID != "" {
additionalClaims["patient_id"] = claims.PatientID
}
@@ -135,6 +154,14 @@ func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
additionalClaims["filter_url"] = claims.FilterURL
}
// Add array claims
if len(claims.StudyIUIDs) > 0 {
additionalClaims["study_iuids"] = claims.StudyIUIDs
}
if len(claims.AccessionNumbers) > 0 {
additionalClaims["accession_numbers"] = claims.AccessionNumbers
}
// Generate a new access token with the same claims
accessToken, err := s.jwtManager.GenerateAccessToken(claims.UserID, claims.Email, claims.Role, claims.UserName, additionalClaims)
if err != nil {

View File

@@ -40,8 +40,10 @@ type CustomClaims struct {
// Patient-specific fields
PatientID string `json:"patient_id,omitempty"`
PatientName string `json:"patient_name,omitempty"`
AccessionNumber string `json:"accession_number,omitempty"`
StudyIUID string `json:"study_iuid,omitempty"`
AccessionNumber string `json:"accession_number,omitempty"` // For backward compatibility
AccessionNumbers []string `json:"accession_numbers,omitempty"` // Multiple accession numbers
StudyIUID string `json:"study_iuid,omitempty"` // For backward compatibility
StudyIUIDs []string `json:"study_iuids,omitempty"` // Multiple study IUIDs
// Navigation and permissions
HomeURL string `json:"home_url,omitempty"`
@@ -52,7 +54,7 @@ type CustomClaims struct {
}
// GenerateAccessToken creates a new access token with role-specific claims
func (m *JWTManager) GenerateAccessToken(userID, email, role, userName string, additionalClaims map[string]string) (string, error) {
func (m *JWTManager) GenerateAccessToken(userID, email, role, userName string, additionalClaims map[string]interface{}) (string, error) {
claims := CustomClaims{
UserID: userID,
Email: email,
@@ -67,27 +69,36 @@ func (m *JWTManager) GenerateAccessToken(userID, email, role, userName string, a
// Add role-specific additional claims
if additionalClaims != nil {
if val, ok := additionalClaims["patient_id"]; ok {
// Handle string claims
if val, ok := additionalClaims["patient_id"].(string); ok {
claims.PatientID = val
}
if val, ok := additionalClaims["patient_name"]; ok {
if val, ok := additionalClaims["patient_name"].(string); ok {
claims.PatientName = val
}
if val, ok := additionalClaims["accession_number"]; ok {
if val, ok := additionalClaims["accession_number"].(string); ok {
claims.AccessionNumber = val
}
if val, ok := additionalClaims["study_iuid"]; ok {
if val, ok := additionalClaims["study_iuid"].(string); ok {
claims.StudyIUID = val
}
if val, ok := additionalClaims["home_url"]; ok {
if val, ok := additionalClaims["home_url"].(string); ok {
claims.HomeURL = val
}
if val, ok := additionalClaims["study_list"]; ok {
if val, ok := additionalClaims["study_list"].(string); ok {
claims.StudyList = val
}
if val, ok := additionalClaims["filter_url"]; ok {
if val, ok := additionalClaims["filter_url"].(string); ok {
claims.FilterURL = val
}
// Handle array claims
if val, ok := additionalClaims["study_iuids"].([]string); ok {
claims.StudyIUIDs = val
}
if val, ok := additionalClaims["accession_numbers"].([]string); ok {
claims.AccessionNumbers = val
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -95,7 +106,7 @@ func (m *JWTManager) GenerateAccessToken(userID, email, role, userName string, a
}
// GenerateRefreshToken creates a new refresh token with the same claims as access token
func (m *JWTManager) GenerateRefreshToken(userID, email, role, userName string, additionalClaims map[string]string) (string, error) {
func (m *JWTManager) GenerateRefreshToken(userID, email, role, userName string, additionalClaims map[string]interface{}) (string, error) {
claims := CustomClaims{
UserID: userID,
Email: email,
@@ -108,29 +119,38 @@ func (m *JWTManager) GenerateRefreshToken(userID, email, role, userName string,
},
}
// Add role-specific additional claims (same as access token)
// Add role-specific additional claims
if additionalClaims != nil {
if val, ok := additionalClaims["patient_id"]; ok {
// Handle string claims
if val, ok := additionalClaims["patient_id"].(string); ok {
claims.PatientID = val
}
if val, ok := additionalClaims["patient_name"]; ok {
if val, ok := additionalClaims["patient_name"].(string); ok {
claims.PatientName = val
}
if val, ok := additionalClaims["accession_number"]; ok {
if val, ok := additionalClaims["accession_number"].(string); ok {
claims.AccessionNumber = val
}
if val, ok := additionalClaims["study_iuid"]; ok {
if val, ok := additionalClaims["study_iuid"].(string); ok {
claims.StudyIUID = val
}
if val, ok := additionalClaims["home_url"]; ok {
if val, ok := additionalClaims["home_url"].(string); ok {
claims.HomeURL = val
}
if val, ok := additionalClaims["study_list"]; ok {
if val, ok := additionalClaims["study_list"].(string); ok {
claims.StudyList = val
}
if val, ok := additionalClaims["filter_url"]; ok {
if val, ok := additionalClaims["filter_url"].(string); ok {
claims.FilterURL = val
}
// Handle array claims
if val, ok := additionalClaims["study_iuids"].([]string); ok {
claims.StudyIUIDs = val
}
if val, ok := additionalClaims["accession_numbers"].([]string); ok {
claims.AccessionNumbers = val
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)