1056 lines
24 KiB
Go
1056 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 milestoneStringValue(value any) string {
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return strings.TrimSpace(text)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|