teg/backend/milestone_api.go
2026-06-11 15:02:19 +02:00

1047 lines
24 KiB
Go

// backend\milestone_api.go
package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
type milestoneRuntimeConfig struct {
Host string
AccessToken string
TokenType string
SkipTLSVerify bool
TokenExpiresAt *time.Time
ServerID string
}
type milestoneHardwarePatchDevice struct {
ID string
InventoryNumber string
MilestoneHardwareID string
MilestoneDisplayName string
IPAddress string
MilestonePort string
MilestoneEnabled bool
}
type updateMilestoneHardwareRequest struct {
Name *string `json:"name"`
DisplayName *string `json:"displayName"`
MilestoneDisplayName *string `json:"milestoneDisplayName"`
IPAddress *string `json:"ipAddress"`
Address *string `json:"address"`
Port *string `json:"port"`
MilestonePort *string `json:"milestonePort"`
Enabled *bool `json:"enabled"`
}
type updateMilestoneChildDeviceRequest struct {
Enabled *bool `json:"enabled"`
}
type milestoneChildPatchDevice struct {
DeviceID string
ChildID string
MilestoneDeviceID string
DeviceType string
Enabled bool
}
type milestoneHardwareUpdateEnvelope struct {
Data map[string]any `json:"data"`
}
const milestoneTokenRefreshBeforeExpiry = 2 * time.Minute
func (s *Server) getMilestoneChildPatchDevice(
ctx context.Context,
deviceID string,
childID string,
) (milestoneChildPatchDevice, error) {
var child milestoneChildPatchDevice
err := s.db.QueryRow(
ctx,
`
SELECT
devices.id::TEXT,
milestone_hardware_child_devices.id::TEXT,
milestone_hardware_child_devices.milestone_device_id,
milestone_hardware_child_devices.device_type,
milestone_hardware_child_devices.enabled
FROM milestone_hardware_child_devices
INNER JOIN devices
ON devices.milestone_hardware_id = milestone_hardware_child_devices.milestone_hardware_id
WHERE devices.id = $1
AND milestone_hardware_child_devices.id = $2
LIMIT 1
`,
deviceID,
childID,
).Scan(
&child.DeviceID,
&child.ChildID,
&child.MilestoneDeviceID,
&child.DeviceType,
&child.Enabled,
)
return child, err
}
func (s *Server) handleUpdateMilestoneChildDevice(w http.ResponseWriter, r *http.Request) {
deviceID := strings.TrimSpace(r.PathValue("id"))
childID := strings.TrimSpace(r.PathValue("childId"))
if deviceID == "" || childID == "" {
writeError(w, http.StatusBadRequest, "Geräte-ID oder Child-ID fehlt")
return
}
var input updateMilestoneChildDeviceRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if input.Enabled == nil {
writeError(w, http.StatusBadRequest, "enabled fehlt")
return
}
child, err := s.getMilestoneChildPatchDevice(r.Context(), deviceID, childID)
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "Milestone-Child-Device wurde nicht gefunden")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Child-Device konnte nicht geladen werden")
return
}
config, err := s.getMilestoneRuntimeConfig(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden")
return
}
collection, err := milestoneCollectionForChildDeviceType(child.DeviceType)
if err != nil {
writeError(w, http.StatusBadRequest, "Unbekannter Milestone-Child-Device-Typ")
return
}
if err := patchMilestoneNamedDevice(
r.Context(),
config,
collection,
child.MilestoneDeviceID,
map[string]any{
"enabled": *input.Enabled,
},
); err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Child-Device konnte nicht aktualisiert werden")
return
}
_, err = s.db.Exec(
r.Context(),
`
UPDATE milestone_hardware_child_devices
SET enabled = $2,
updated_at = now()
WHERE id = $1
`,
child.ChildID,
*input.Enabled,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Child-Device konnte lokal nicht aktualisiert werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"id": child.ChildID,
"enabled": *input.Enabled,
})
}
func milestoneCollectionForChildDeviceType(deviceType string) (string, error) {
switch strings.TrimSpace(strings.ToLower(deviceType)) {
case "camera", "cameras":
return "cameras", nil
case "microphone", "microphones":
return "microphones", nil
default:
return "", fmt.Errorf("unsupported milestone child device type: %s", deviceType)
}
}
func (s *Server) getMilestoneHardwarePatchDevice(
ctx context.Context,
deviceID string,
) (milestoneHardwarePatchDevice, error) {
var device milestoneHardwarePatchDevice
err := s.db.QueryRow(
ctx,
`
SELECT
id::TEXT,
inventory_number,
COALESCE(milestone_hardware_id, ''),
COALESCE(milestone_display_name, ''),
COALESCE(ip_address, ''),
COALESCE(milestone_port, ''),
milestone_enabled
FROM devices
WHERE id = $1
LIMIT 1
`,
deviceID,
).Scan(
&device.ID,
&device.InventoryNumber,
&device.MilestoneHardwareID,
&device.MilestoneDisplayName,
&device.IPAddress,
&device.MilestonePort,
&device.MilestoneEnabled,
)
return device, err
}
func (s *Server) handleUpdateMilestoneHardware(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
deviceID := strings.TrimSpace(r.PathValue("id"))
if deviceID == "" {
writeError(w, http.StatusBadRequest, "Geräte-ID fehlt")
return
}
var input updateMilestoneHardwareRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if input.Name == nil &&
input.DisplayName == nil &&
input.MilestoneDisplayName == nil &&
input.IPAddress == nil &&
input.Address == nil &&
input.Port == nil &&
input.MilestonePort == nil &&
input.Enabled == nil {
writeError(w, http.StatusBadRequest, "Keine Milestone-Felder zum Aktualisieren übergeben")
return
}
device, err := s.getMilestoneHardwarePatchDevice(r.Context(), deviceID)
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "Gerät wurde nicht gefunden")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "Gerät konnte nicht geladen werden")
return
}
if strings.TrimSpace(device.MilestoneHardwareID) == "" {
writeError(w, http.StatusBadRequest, "Dieses Gerät ist kein Milestone-Gerät")
return
}
nextDisplayName := strings.TrimSpace(device.MilestoneDisplayName)
nextIPAddress := strings.TrimSpace(device.IPAddress)
nextPort := strings.TrimSpace(device.MilestonePort)
nextEnabled := device.MilestoneEnabled
payload := map[string]any{}
changes := []DeviceHistoryChange{}
requestedDisplayName := input.Name
if requestedDisplayName == nil {
requestedDisplayName = input.DisplayName
}
if requestedDisplayName == nil {
requestedDisplayName = input.MilestoneDisplayName
}
if requestedDisplayName != nil {
nextDisplayName = strings.TrimSpace(*requestedDisplayName)
payload["name"] = nextDisplayName
if nextDisplayName != strings.TrimSpace(device.MilestoneDisplayName) {
changes = appendHistoryChange(
changes,
"milestoneDisplayName",
"Milestone-Anzeigename",
device.MilestoneDisplayName,
nextDisplayName,
)
}
}
requestedAddress := input.Address
if requestedAddress == nil {
requestedAddress = input.IPAddress
}
requestedPort := input.Port
if requestedPort == nil {
requestedPort = input.MilestonePort
}
if requestedAddress != nil || requestedPort != nil {
addressValue := device.IPAddress
if requestedAddress != nil {
addressValue = *requestedAddress
}
portValue := device.MilestonePort
if requestedPort != nil {
portValue = *requestedPort
}
nextAddress := normalizeMilestoneHardwareAddress(addressValue, portValue)
nextIPAddress, parsedPort := extractMilestoneHardwareAddressParts(nextAddress)
nextPort = normalizeMilestonePort(portValue)
if nextPort == "" {
nextPort = parsedPort
}
if nextIPAddress == "" {
nextIPAddress = strings.TrimSpace(addressValue)
}
payload["address"] = nextAddress
if nextIPAddress != strings.TrimSpace(device.IPAddress) {
changes = appendHistoryChange(
changes,
"ipAddress",
"IP-Adresse",
device.IPAddress,
nextIPAddress,
)
}
if nextPort != strings.TrimSpace(device.MilestonePort) {
changes = appendHistoryChange(
changes,
"milestonePort",
"Milestone-Port",
device.MilestonePort,
nextPort,
)
}
}
if input.Enabled != nil {
nextEnabled = *input.Enabled
payload["enabled"] = nextEnabled
if nextEnabled != device.MilestoneEnabled {
changes = appendHistoryChange(
changes,
"milestoneEnabled",
"Milestone-Status",
formatHistoryBool(device.MilestoneEnabled),
formatHistoryBool(nextEnabled),
)
}
}
if len(payload) == 0 {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"id": device.ID,
"milestoneDisplayName": device.MilestoneDisplayName,
"ipAddress": device.IPAddress,
"milestonePort": device.MilestonePort,
"milestoneEnabled": device.MilestoneEnabled,
})
return
}
config, err := s.getMilestoneRuntimeConfig(r.Context())
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusBadRequest, "Milestone-Einstellungen sind noch nicht hinterlegt")
return
}
if err != nil {
s.logAndWriteUserError(
w,
r,
user,
http.StatusBadGateway,
"milestone_hardware",
"load_or_refresh_token",
"Milestone-Token konnte nicht geladen oder erneuert werden",
err,
LogFields{
"deviceId": device.ID,
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
},
)
return
}
if strings.TrimSpace(config.Host) == "" {
writeError(w, http.StatusBadRequest, "Milestone Host fehlt")
return
}
if strings.TrimSpace(config.AccessToken) == "" {
writeError(w, http.StatusBadRequest, "Milestone-Token fehlt und konnte nicht automatisch erneuert werden")
return
}
if err := updateMilestoneHardware(
r.Context(),
config,
device.MilestoneHardwareID,
payload,
device.MilestoneDisplayName,
nextDisplayName,
); err != nil {
s.logAndWriteUserError(
w,
r,
user,
http.StatusBadGateway,
"milestone_hardware",
"update_hardware",
"Milestone-Gerät konnte nicht aktualisiert werden",
err,
LogFields{
"deviceId": device.ID,
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
"payload": payload,
},
)
return
}
tx, err := s.db.Begin(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden")
return
}
defer tx.Rollback(r.Context())
_, err = tx.Exec(
r.Context(),
`
UPDATE devices
SET
milestone_display_name = $2,
ip_address = $3,
milestone_port = $4,
milestone_enabled = $5,
milestone_synced_at = now(),
updated_at = now()
WHERE id = $1
`,
device.ID,
nextDisplayName,
nextIPAddress,
nextPort,
nextEnabled,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden")
return
}
_, err = tx.Exec(
r.Context(),
`
UPDATE milestone_hardware_child_devices
SET
name = CASE
WHEN name = $2 THEN $3
WHEN name LIKE $2 || ' - %' THEN $3 || substring(name FROM char_length($2) + 1)
WHEN name LIKE $2 || ' %' THEN $3 || substring(name FROM char_length($2) + 1)
ELSE name
END,
display_name = CASE
WHEN display_name = $2 THEN $3
WHEN display_name LIKE $2 || ' - %' THEN $3 || substring(display_name FROM char_length($2) + 1)
WHEN display_name LIKE $2 || ' %' THEN $3 || substring(display_name FROM char_length($2) + 1)
ELSE display_name
END,
short_name = CASE
WHEN short_name = $2 THEN $3
WHEN short_name LIKE $2 || ' - %' THEN $3 || substring(short_name FROM char_length($2) + 1)
WHEN short_name LIKE $2 || ' %' THEN $3 || substring(short_name FROM char_length($2) + 1)
ELSE short_name
END,
updated_at = now()
WHERE milestone_hardware_id = $1
`,
device.MilestoneHardwareID,
device.MilestoneDisplayName,
nextDisplayName,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Child-Devices konnten lokal nicht aktualisiert werden")
return
}
if len(changes) > 0 {
if err := recordDeviceHistory(r.Context(), tx, device.ID, user, "updated", changes); err != nil {
writeError(w, http.StatusInternalServerError, "Geräteverlauf konnte nicht gespeichert werden")
return
}
}
if err := tx.Commit(r.Context()); err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden")
return
}
if err := s.publishDeviceChangeEvent(
r.Context(),
user.ID,
device.ID,
"device.milestone.hardware",
"Milestone-Gerät aktualisiert",
formatText("Gerät %s wurde in Milestone aktualisiert.", device.InventoryNumber),
LogFields{
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
"payload": payload,
},
); err != nil {
logError("Device-SSE-Event konnte nicht gesendet werden", err, LogFields{
"deviceId": device.ID,
"type": "device.milestone.hardware",
})
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"id": device.ID,
"milestoneDisplayName": nextDisplayName,
"ipAddress": nextIPAddress,
"milestonePort": nextPort,
"milestoneEnabled": nextEnabled,
})
}
func normalizeMilestoneHardwareAddress(value string, port string) string {
value = strings.TrimSpace(value)
port = normalizeMilestonePort(port)
if value == "" || value == "-" {
return ""
}
rawValue := value
if !strings.HasPrefix(strings.ToLower(value), "http://") &&
!strings.HasPrefix(strings.ToLower(value), "https://") {
value = "http://" + strings.TrimRight(value, "/")
}
parsedURL, err := url.Parse(value)
if err != nil || parsedURL.Hostname() == "" {
host := strings.TrimRight(rawValue, "/")
host = strings.TrimPrefix(host, "http://")
host = strings.TrimPrefix(host, "https://")
if existingHost, existingPort, found := strings.Cut(host, ":"); found {
host = strings.TrimSpace(existingHost)
if port == "" && isOnlyDigits(strings.TrimSpace(existingPort)) {
port = strings.TrimSpace(existingPort)
}
}
if port == "" {
port = defaultMilestonePortForScheme("http")
}
scheme := milestoneSchemeForPort(port)
return scheme + "://" + host + ":" + port + "/"
}
host := parsedURL.Hostname()
if port == "" {
port = normalizeMilestonePort(parsedURL.Port())
}
if port == "" {
port = defaultMilestonePortForScheme(parsedURL.Scheme)
}
scheme := milestoneSchemeForPort(port)
return scheme + "://" + host + ":" + port + "/"
}
func milestoneSchemeForPort(port string) string {
port = normalizeMilestonePort(port)
if port == "443" {
return "https"
}
return "http"
}
func normalizeMilestonePort(value string) string {
value = strings.TrimSpace(value)
value = strings.TrimPrefix(value, ":")
if value == "-" {
return ""
}
return value
}
func (s *Server) getMilestoneRuntimeConfig(
ctx context.Context,
) (milestoneRuntimeConfig, error) {
config, err := s.loadMilestoneRuntimeConfig(ctx)
if err != nil {
return config, err
}
if shouldRefreshMilestoneToken(config) {
if err := s.refreshMilestoneRuntimeToken(ctx); err != nil {
return config, err
}
return s.loadMilestoneRuntimeConfig(ctx)
}
return config, nil
}
func shouldRefreshMilestoneToken(config milestoneRuntimeConfig) bool {
if strings.TrimSpace(config.AccessToken) == "" {
return true
}
if config.TokenExpiresAt == nil {
return true
}
return time.Now().After(config.TokenExpiresAt.Add(-milestoneTokenRefreshBeforeExpiry))
}
func (s *Server) loadMilestoneRuntimeConfig(
ctx context.Context,
) (milestoneRuntimeConfig, error) {
var config milestoneRuntimeConfig
var tokenExpiresAt sql.NullTime
err := s.db.QueryRow(
ctx,
`
SELECT
host,
COALESCE(pgp_sym_decrypt(access_token_encrypted, $1), ''),
COALESCE(token_type, ''),
skip_tls_verify,
token_expires_at,
COALESCE(server_id, '')
FROM milestone_settings
WHERE id = 1
LIMIT 1
`,
s.milestoneEncryptionKey(),
).Scan(
&config.Host,
&config.AccessToken,
&config.TokenType,
&config.SkipTLSVerify,
&tokenExpiresAt,
&config.ServerID,
)
if err != nil {
return config, err
}
if tokenExpiresAt.Valid {
config.TokenExpiresAt = &tokenExpiresAt.Time
}
return config, nil
}
func (s *Server) refreshMilestoneRuntimeToken(ctx context.Context) error {
credentials, err := s.getMilestoneCredentials(ctx)
if err != nil {
return err
}
if credentials.Host == "" || credentials.Username == "" || credentials.Password == "" {
return errors.New("Milestone-Zugangsdaten sind unvollständig")
}
tokenResponse, err := s.requestMilestoneToken(ctx, credentials)
if err != nil {
return err
}
accessToken := strings.TrimSpace(tokenResponse.AccessToken)
if accessToken == "" {
return errors.New("Milestone hat keinen Access-Token zurückgegeben")
}
tokenType := strings.TrimSpace(tokenResponse.TokenType)
if tokenType == "" {
tokenType = "Bearer"
}
expiresIn := tokenResponse.ExpiresIn
if expiresIn <= 0 {
expiresIn = 3600
}
tokenExpiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
_, err = s.db.Exec(
ctx,
`
UPDATE milestone_settings
SET
access_token_encrypted = pgp_sym_encrypt($1, $2),
token_type = $3,
token_expires_at = $4,
token_updated_at = now(),
updated_at = now()
WHERE id = 1
`,
accessToken,
s.milestoneEncryptionKey(),
tokenType,
tokenExpiresAt,
)
return err
}
func updateMilestoneHardware(
ctx context.Context,
config milestoneRuntimeConfig,
hardwareID string,
updates map[string]any,
previousHardwareName string,
requestedHardwareName string,
) error {
currentHardware, err := getMilestoneHardware(ctx, config, hardwareID)
if err != nil {
return err
}
currentHardwareName := milestoneStringValue(currentHardware["name"])
if currentHardwareName == "" {
currentHardwareName = milestoneStringValue(currentHardware["displayName"])
}
oldHardwareName := strings.TrimSpace(previousHardwareName)
if oldHardwareName == "" {
oldHardwareName = currentHardwareName
}
newHardwareName := strings.TrimSpace(requestedHardwareName)
if value, exists := updates["name"]; exists {
if updateName := milestoneStringValue(value); updateName != "" {
newHardwareName = updateName
}
}
_, shouldUpdateDeviceNames := updates["name"]
hardwareDevices := []milestoneNamedDevice{}
if shouldUpdateDeviceNames {
embeddedDevices := milestoneNamedDevicesFromHardware(currentHardware)
camerasWithDisabled, err := requestMilestoneNamedDevicesByHardware(
ctx,
config,
"cameras",
hardwareID,
)
if err != nil {
return err
}
microphonesWithDisabled, err := requestMilestoneNamedDevicesByHardware(
ctx,
config,
"microphones",
hardwareID,
)
if err != nil {
return err
}
hardwareDevices = mergeMilestoneNamedDevices(
embeddedDevices,
camerasWithDisabled,
microphonesWithDisabled,
)
}
for key, value := range updates {
currentHardware[key] = value
}
if err := putMilestoneHardware(ctx, config, hardwareID, currentHardware); err != nil {
return err
}
if !shouldUpdateDeviceNames || newHardwareName == "" || len(hardwareDevices) == 0 {
return nil
}
return updateMilestoneChildDeviceBaseNames(
ctx,
config,
hardwareDevices,
oldHardwareName,
newHardwareName,
)
}
func getMilestoneHardware(
ctx context.Context,
config milestoneRuntimeConfig,
hardwareID string,
) (map[string]any, error) {
requestURL := strings.TrimRight(config.Host, "/") +
"/API/rest/v1/hardware/" +
url.PathEscape(hardwareID)
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
requestURL,
nil,
)
if err != nil {
return nil, err
}
tokenType := strings.TrimSpace(config.TokenType)
if tokenType == "" {
tokenType = "Bearer"
}
request.Header.Set("Authorization", tokenType+" "+config.AccessToken)
request.Header.Set("Accept", "application/json")
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, newAppErrorf(
"milestone hardware get failed",
LogFields{
"statusCode": response.StatusCode,
"responseBody": string(responseBody),
"hardwareId": hardwareID,
},
"milestone hardware get failed: status=%d body=%s",
response.StatusCode,
string(responseBody),
)
}
var envelope milestoneHardwareUpdateEnvelope
if err := json.Unmarshal(responseBody, &envelope); err == nil && envelope.Data != nil {
return envelope.Data, nil
}
var direct map[string]any
if err := json.Unmarshal(responseBody, &direct); err != nil {
return nil, err
}
if data, ok := direct["data"].(map[string]any); ok {
return data, nil
}
return direct, nil
}
func putMilestoneHardware(
ctx context.Context,
config milestoneRuntimeConfig,
hardwareID string,
payload map[string]any,
) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
requestURL := strings.TrimRight(config.Host, "/") +
"/API/rest/v1/hardware/" +
url.PathEscape(hardwareID)
request, err := http.NewRequestWithContext(
ctx,
http.MethodPut,
requestURL,
bytes.NewReader(body),
)
if err != nil {
return err
}
tokenType := strings.TrimSpace(config.TokenType)
if tokenType == "" {
tokenType = "Bearer"
}
request.Header.Set("Authorization", tokenType+" "+config.AccessToken)
request.Header.Set("Accept", "application/json")
request.Header.Set("Content-Type", "application/json")
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
if err != nil {
return err
}
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode >= 300 {
return newAppErrorf(
"milestone hardware put failed",
LogFields{
"statusCode": response.StatusCode,
"responseBody": string(responseBody),
"hardwareId": hardwareID,
"payload": payload,
},
"milestone hardware put failed: status=%d body=%s",
response.StatusCode,
string(responseBody),
)
}
return nil
}
func getMilestoneNamedDevice(
ctx context.Context,
config milestoneRuntimeConfig,
collection string,
deviceID string,
) (map[string]any, error) {
requestURL := strings.TrimRight(config.Host, "/") +
"/API/rest/v1/" + collection + "/" + url.PathEscape(deviceID)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
if err != nil {
return nil, err
}
tokenType := strings.TrimSpace(config.TokenType)
if tokenType == "" {
tokenType = "Bearer"
}
request.Header.Set("Authorization", tokenType+" "+config.AccessToken)
request.Header.Set("Accept", "application/json")
client := milestoneHTTPClient(config.SkipTLSVerify)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("milestone %s get failed: status=%d body=%s", collection, response.StatusCode, string(body))
}
var envelope struct {
Data map[string]any `json:"data"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, err
}
if envelope.Data == nil {
return map[string]any{}, nil
}
return envelope.Data, nil
}
func milestoneBoolValue(value any) bool {
switch typedValue := value.(type) {
case bool:
return typedValue
default:
return false
}
}
func milestoneIntPointerValue(value any) (*int, bool) {
switch typedValue := value.(type) {
case int:
v := typedValue
return &v, true
case int32:
v := int(typedValue)
return &v, true
case int64:
v := int(typedValue)
return &v, true
case float64:
v := int(typedValue)
return &v, true
default:
return nil, false
}
}