patient see their multiple studies
This commit is contained in:
@@ -61,10 +61,66 @@ 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
|
||||
queryParams.Set("StudyInstanceUID", claims.StudyIUID)
|
||||
// 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
|
||||
|
||||
@@ -187,21 +187,49 @@ 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 {
|
||||
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))
|
||||
if requestedStudyUID != "" {
|
||||
// Check if the requested study is authorized
|
||||
isAuthorized := false
|
||||
|
||||
// Return 403 Forbidden with a clear message
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Access denied: You do not have permission to view this study",
|
||||
"code": "forbidden_study_access",
|
||||
})
|
||||
return
|
||||
// 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("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")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Access denied: You do not have permission to view this study",
|
||||
"code": "forbidden_study_access",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Patient has access or is requesting a list (which will be filtered)
|
||||
|
||||
@@ -38,12 +38,14 @@ var MockUsers = []User{
|
||||
|
||||
// PatientData represents additional data for patients
|
||||
type PatientData struct {
|
||||
PatientID string `json:"patient_id"`
|
||||
UserID string `json:"user_id"`
|
||||
StudyIUID string `json:"study_iuid"`
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
PatientName string `json:"patient_name"`
|
||||
ReferringPhysician string `json:"referring_physician"`
|
||||
PatientID string `json:"patient_id"`
|
||||
UserID string `json:"user_id"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// MockPatients represents a mock database of patient data
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -23,10 +23,12 @@ type RefreshToken struct {
|
||||
|
||||
// PatientDetails contains patient-specific data
|
||||
type PatientDetails struct {
|
||||
PatientID string `json:"patient_id"`
|
||||
PatientName string `json:"patient_name"`
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
StudyInstanceUID string `json:"study_instance_uid"`
|
||||
PatientID string `json:"patient_id"`
|
||||
PatientName string `json:"patient_name"`
|
||||
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
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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"
|
||||
|
||||
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUID)
|
||||
// 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 {
|
||||
|
||||
@@ -38,10 +38,12 @@ type CustomClaims struct {
|
||||
TokenType string `json:"token_type"` // access or refresh
|
||||
|
||||
// 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"`
|
||||
PatientID string `json:"patient_id,omitempty"`
|
||||
PatientName string `json:"patient_name,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)
|
||||
|
||||
Reference in New Issue
Block a user