339 lines
6.9 KiB
Go
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
|
|
}
|