inou/portal/api_client.go

234 lines
6.1 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"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: ser.SeriesDesc,
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
}