teg/backend/milestone_hardware.go
2026-05-22 14:11:18 +02:00

339 lines
6.9 KiB
Go

// backend/milestone_hardware.go
package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"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
}
type milestoneToggleDevice struct {
ID string
InventoryNumber string
MilestoneHardwareID string
MilestoneEnabled bool
}
func (s *Server) handleToggleMilestoneDeviceStatus(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
}
device, err := s.getMilestoneToggleDevice(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 device.MilestoneHardwareID == "" {
writeError(w, http.StatusBadRequest, "Dieses Gerät ist kein Milestone-Gerät")
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 {
writeError(w, http.StatusInternalServerError, "Milestone-Einstellungen konnten nicht geladen werden")
return
}
if config.Host == "" || config.AccessToken == "" {
writeError(w, http.StatusBadRequest, "Milestone Host oder Token fehlt")
return
}
if config.TokenExpiresAt != nil && time.Now().After(config.TokenExpiresAt.Add(-30*time.Second)) {
writeError(w, http.StatusBadRequest, "Milestone-Token ist abgelaufen. Bitte Token neu abrufen.")
return
}
nextEnabled := !device.MilestoneEnabled
if err := patchMilestoneHardware(
r.Context(),
config,
device.MilestoneHardwareID,
map[string]bool{
"enabled": nextEnabled,
},
); err != nil {
s.logAndWriteUserError(
w,
r,
user,
http.StatusBadGateway,
"milestone_hardware",
"patch_hardware",
"Milestone-Status konnte nicht geändert werden",
err,
LogFields{
"deviceId": device.ID,
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
"currentEnabled": device.MilestoneEnabled,
"nextEnabled": nextEnabled,
"payload": LogFields{
"enabled": nextEnabled,
},
},
)
return
}
_, err = s.db.Exec(
r.Context(),
`
UPDATE devices
SET
milestone_enabled = $2,
milestone_synced_at = now(),
updated_at = now()
WHERE id = $1
`,
device.ID,
nextEnabled,
)
if err != nil {
s.logAndWriteUserError(
w,
r,
user,
http.StatusInternalServerError,
"milestone_hardware",
"update_local_device",
"Milestone-Status konnte lokal nicht aktualisiert werden",
err,
LogFields{
"deviceId": device.ID,
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
"nextEnabled": nextEnabled,
},
)
return
}
oldValue := "Aktiviert"
newValue := "Deaktiviert"
if nextEnabled {
oldValue = "Deaktiviert"
newValue = "Aktiviert"
}
changes, _ := json.Marshal([]map[string]string{
{
"field": "milestoneEnabled",
"label": "Milestone-Status",
"oldValue": oldValue,
"newValue": newValue,
},
})
_, _ = s.db.Exec(
r.Context(),
`
INSERT INTO device_history (
device_id,
user_id,
action,
changes
)
VALUES ($1, $2, 'updated', $3)
`,
device.ID,
user.ID,
changes,
)
if err := s.publishDeviceChangeEvent(
r.Context(),
user.ID,
device.ID,
"device.milestone.status",
"Milestone-Status geändert",
formatText(
"Gerät %s wurde in Milestone %s.",
device.InventoryNumber,
strings.ToLower(newValue),
),
LogFields{
"inventoryNumber": device.InventoryNumber,
"milestoneHardwareId": device.MilestoneHardwareID,
"milestoneEnabled": nextEnabled,
},
); err != nil {
logError("Device-SSE-Event konnte nicht gesendet werden", err, LogFields{
"deviceId": device.ID,
"type": "device.milestone.status",
})
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"id": device.ID,
"milestoneEnabled": nextEnabled,
})
}
func (s *Server) getMilestoneToggleDevice(
ctx context.Context,
deviceID string,
) (milestoneToggleDevice, error) {
var device milestoneToggleDevice
err := s.db.QueryRow(
ctx,
`
SELECT
id::TEXT,
inventory_number,
COALESCE(milestone_hardware_id, ''),
milestone_enabled
FROM devices
WHERE id = $1
LIMIT 1
`,
deviceID,
).Scan(
&device.ID,
&device.InventoryNumber,
&device.MilestoneHardwareID,
&device.MilestoneEnabled,
)
return device, err
}
func (s *Server) getMilestoneRuntimeConfig(
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
FROM milestone_settings
WHERE id = 1
LIMIT 1
`,
s.milestoneEncryptionKey(),
).Scan(
&config.Host,
&config.AccessToken,
&config.TokenType,
&config.SkipTLSVerify,
&tokenExpiresAt,
)
if err != nil {
return config, err
}
if tokenExpiresAt.Valid {
config.TokenExpiresAt = &tokenExpiresAt.Time
}
return config, nil
}
func patchMilestoneHardware(
ctx context.Context,
config milestoneRuntimeConfig,
hardwareID string,
payload 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.MethodPatch,
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 patch failed",
LogFields{
"statusCode": response.StatusCode,
"responseBody": string(responseBody),
"hardwareId": hardwareID,
},
"milestone hardware patch failed: status=%d body=%s",
response.StatusCode,
string(responseBody),
)
}
return nil
}