package handlers import ( "fmt" "io" "net/http" "net/url" "strings" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/middleware" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/proxy" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) // DicomHandler handles DICOM Web requests type DicomHandler struct { client *proxy.Client logger *zap.Logger } // NewDicomHandler creates a new DICOM handler func NewDicomHandler(client *proxy.Client, logger *zap.Logger) *DicomHandler { return &DicomHandler{ client: client, logger: logger, } } // buildRefDoctorFilter constructs a properly encoded query string for referring doctor filtering func (h *DicomHandler) buildRefDoctorFilter(doctorName string, queryParams url.Values) string { // Extract basic parameters with fallbacks limit := queryParams.Get("limit") if limit == "" { limit = "101" // Default limit used by OHIF } offset := queryParams.Get("offset") if offset == "" { offset = "0" // Default offset } includeField := queryParams.Get("includefield") // Make sure includefield includes 00080090 (ReferringPhysician) if includeField != "" && !strings.Contains(includeField, "00080090") { includeField = includeField + ",00080090" } else if includeField == "" { includeField = "00081030,00080060,00080090" } // Properly encode the doctor's name encodedName := strings.ReplaceAll(doctorName, " ", "%20") encodedName = strings.ReplaceAll(encodedName, ",", "%2C") encodedName = strings.ReplaceAll(encodedName, ".", "%2E") // Construct query string manually to avoid double-encoding return fmt.Sprintf("limit=%s&offset=%s&fuzzymatching=false&includefield=%s&00080090=%s", limit, offset, includeField, encodedName) } // isStudyListRequest checks if a path refers to the top-level studies endpoint func isStudyListRequest(path string) bool { // Normalize the path first if !strings.HasPrefix(path, "/") { path = "/" + path } // Check for exact match with "/studies" return path == "/studies" } // ForwardRequest forwards the request to Google Healthcare API func (h *DicomHandler) ForwardRequest(w http.ResponseWriter, r *http.Request) { // Get claims from context if they exist var claims *auth.CustomClaims claimsValue := r.Context().Value(middleware.ClaimsKey) // Add detailed debug logging about claims if claimsValue == nil { h.logger.Warn("Claims not found in context", zap.String("path", r.URL.Path), zap.String("method", r.Method)) } else { claims = claimsValue.(*auth.CustomClaims) h.logger.Debug("Claims retrieved from context", zap.String("userID", claims.UserID), zap.String("role", claims.Role), zap.String("userName", claims.UserName)) } // Get the path after /dicomWeb urlPath := chi.URLParam(r, "*") // If the URL parameter is empty, try to extract it from the URL path if urlPath == "" { // Remove /dicomWeb prefix from the URL prefix := "/dicomWeb" if strings.HasPrefix(r.URL.Path, prefix) { urlPath = r.URL.Path[len(prefix):] } } // Normalize the path if !strings.HasPrefix(urlPath, "/") { urlPath = "/" + urlPath } h.logger.Debug("Forwarding request", zap.String("path", urlPath), zap.String("method", r.Method), zap.String("url", r.URL.String())) // Copy query parameters queryParams := r.URL.Query() queryString := "" // Apply role-specific query modifications if claims != nil { switch claims.Role { case "patient": // For patients requesting study list, filter to only show their studies if isStudyListRequest(urlPath) { // Check if 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 studies", zap.Strings("studies", claims.StudyIUIDs)) } } 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 // Check StudyIUIDs array for _, id := range claims.StudyIUIDs { if id == studyID { authorized = true break } } // 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), ) } } // Use standard query parameter encoding queryString = queryParams.Encode() case "ref_doctor": // For ref_doctor requesting study list, apply filter if isStudyListRequest(urlPath) { // Use our helper function to build the properly encoded query queryString = h.buildRefDoctorFilter(claims.UserName, queryParams) h.logger.Debug("Applied referring physician filter", zap.String("doctorName", claims.UserName), zap.String("queryString", queryString)) } else { // For other paths, use standard query parameter encoding queryString = queryParams.Encode() } case "expertise_doctor": // No restrictions for expertise_doctor queryString = queryParams.Encode() } } else { // No claims, use standard query parameter encoding queryString = queryParams.Encode() } // Add the query string to the path if queryString != "" { urlPath = urlPath + "?" + queryString } // Read request body if present var bodyBytes []byte if r.Body != nil && r.ContentLength > 0 { var err error bodyBytes, err = io.ReadAll(r.Body) if err != nil { h.logger.Error("Failed to read request body", zap.Error(err)) http.Error(w, "Failed to read request body", http.StatusInternalServerError) return } } // Get request headers headers := make(map[string]string) for k, v := range r.Header { if len(v) > 0 { headers[k] = v[0] } } // Forward the request to Healthcare API response, err := h.client.ForwardRequest( r.Context(), r.Method, urlPath, headers, bodyBytes) if err != nil { h.logger.Error("Request forwarding failed", zap.Error(err)) http.Error(w, fmt.Sprintf("Request failed: %v", err), http.StatusInternalServerError) return } // Set response headers for k, v := range response.Headers { w.Header().Set(k, v) } // Set status and write response body w.WriteHeader(response.StatusCode) w.Write(response.Body) }