package main import ( "bytes" "encoding/json" "fmt" "html" "io" "net/http" "sort" "time" ) var apiClient = &http.Client{Timeout: 5 * time.Second} func apiGet(path string) ([]byte, error) { resp, err := apiClient.Get(apiBackend + path) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } func apiPost(path string, body interface{}) ([]byte, error) { jsonData, err := json.Marshal(body) if err != nil { return nil, err } resp, err := apiClient.Post(apiBackend+path, "application/json", bytes.NewReader(jsonData)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) } return io.ReadAll(resp.Body) } // ============================================================================ // Dossier operations // ============================================================================ type DossierLookup struct { ID string `json:"id"` Name string `json:"name"` DOB string `json:"dob"` Found bool `json:"found"` } type DossierCreateResponse struct { ID string `json:"id"` Exists bool `json:"exists"` } func getDossierByEmailHash(emailHash string) (DossierLookup, bool) { data, err := apiGet("/api/dossier?email_hash=" + emailHash) if err != nil { return DossierLookup{}, false } var resp DossierLookup json.Unmarshal(data, &resp) return resp, resp.Found } func createDossierForInvite(emailHash, email, invitedByHex string) (string, bool, error) { data, err := apiPost("/api/dossier", map[string]string{ "email_hash": emailHash, "email": email, "invited_by": invitedByHex, }) if err != nil { return "", false, err } var resp DossierCreateResponse json.Unmarshal(data, &resp) return resp.ID, resp.Exists, nil } // ============================================================================ // Access management // ============================================================================ type AccessRecord struct { Accessor string `json:"accessor"` Target string `json:"target"` Name string `json:"name"` Relation int `json:"relation"` IsCareReceiver bool `json:"is_care_receiver"` CanEdit bool `json:"can_edit"` } // getAccess returns access record and whether it was found func getAccess(accessorHex, targetHex string) (AccessRecord, bool) { data, err := apiGet(fmt.Sprintf("/api/access?accessor=%s&target=%s", accessorHex, targetHex)) if err != nil { return AccessRecord{}, false } var check map[string]interface{} json.Unmarshal(data, &check) if found, ok := check["found"]; ok && found == false { return AccessRecord{}, false } var rec AccessRecord json.Unmarshal(data, &rec) return rec, true } func listAccessors(targetHex string) ([]AccessRecord, error) { data, err := apiGet("/api/access?target=" + targetHex) if err != nil { return nil, err } var records []AccessRecord json.Unmarshal(data, &records) return records, nil } func grantAccess(accessorHex, targetHex string, relation int, isCareReceiver, canEdit bool) error { _, err := apiPost("/api/access", map[string]interface{}{ "accessor": accessorHex, "target": targetHex, "relation": relation, "is_care_receiver": isCareReceiver, "can_edit": canEdit, }) return err } func revokeAccess(accessorHex, targetHex string) error { _, err := apiPost("/api/access", map[string]interface{}{ "accessor": accessorHex, "target": targetHex, "delete": true, }) return err } func touchAccess(accessorHex, targetHex string) error { _, err := apiPost("/api/access", map[string]interface{}{ "accessor": accessorHex, "target": targetHex, "touch": true, }) return err } // ============================================================================ // Studies/Series (imaging) // ============================================================================ func fetchStudiesWithSeries(dossierHex string) ([]Study, error) { data, err := apiGet("/api/studies?dossier=" + dossierHex) if err != nil { return nil, err } var apiStudies []struct { ID string `json:"id"` PatientName string `json:"patient_name"` StudyDate string `json:"study_date"` StudyDesc string `json:"study_desc"` SeriesCount int `json:"series_count"` } if err := json.Unmarshal(data, &apiStudies); err != nil { return nil, err } var studies []Study for _, as := range apiStudies { s := Study{ ID: as.ID, Date: as.StudyDate, Description: as.StudyDesc, SeriesCount: as.SeriesCount, } seriesData, err := apiGet(fmt.Sprintf("/api/series?dossier=%s&study=%s", dossierHex, as.ID)) if err == nil { var apiSeries []struct { ID string `json:"id"` SeriesNumber int `json:"series_number"` SeriesDesc string `json:"series_desc"` Modality string `json:"modality"` SliceCount int `json:"slice_count"` } if json.Unmarshal(seriesData, &apiSeries) == nil { for _, ser := range apiSeries { s.Series = append(s.Series, Series{ ID: ser.ID, Description: html.EscapeString(ser.SeriesDesc), // FIX TASK-019: XSS prevention Modality: ser.Modality, SliceCount: ser.SliceCount, }) s.SliceCount += ser.SliceCount if s.Modality == "" { s.Modality = ser.Modality } } } } sort.Slice(s.Series, func(i, j int) bool { return s.Series[i].Description < s.Series[j].Description }) s.SeriesCount = len(s.Series) studies = append(studies, s) } sort.Slice(studies, func(i, j int) bool { if studies[i].Date != studies[j].Date { return studies[i].Date > studies[j].Date } return studies[i].Description < studies[j].Description }) return studies, nil } // ============================================================================ // Audit logging // ============================================================================ func addAudit(dossierHex, action, targetHex, details string) error { _, err := apiPost("/api/audit", map[string]interface{}{ "dossier": dossierHex, "action": action, "target": targetHex, "details": details, }) return err }