235 lines
6.1 KiB
Go
235 lines
6.1 KiB
Go
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
|
|
}
|