mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 21:10:13 +00:00
Implements REST API endpoints to enable automatic registration of temperature proxies during sensor-proxy installation. API endpoints: - POST /api/temperature-proxy/register - Accepts: hostname, proxy_url - Returns: authentication token - Finds matching PVE instance and configures proxy URL/token - No authentication required (called during installation) - DELETE /api/temperature-proxy/unregister?hostname=X - Removes proxy configuration from PVE instance - Requires admin authentication Implementation: - Uses config.ConfigPersistence for loading/saving nodes.enc - Matches PVE instances by hostname in Host field or ClusterEndpoints - Generates cryptographically secure random tokens (32 bytes, base64) - Atomic config updates (load → modify → save) Next step: Update install-sensor-proxy.sh to call registration API Related to #571
226 lines
7.3 KiB
Go
226 lines
7.3 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// TemperatureProxyHandlers manages temperature proxy registration
|
|
type TemperatureProxyHandlers struct {
|
|
persistence *config.ConfigPersistence
|
|
}
|
|
|
|
// NewTemperatureProxyHandlers constructs a new handler set for temperature proxy
|
|
func NewTemperatureProxyHandlers(persistence *config.ConfigPersistence) *TemperatureProxyHandlers {
|
|
return &TemperatureProxyHandlers{persistence: persistence}
|
|
}
|
|
|
|
// HandleRegister handles temperature proxy registration from the installer
|
|
//
|
|
// POST /api/temperature-proxy/register
|
|
// Body: {"hostname": "pve1", "proxy_url": "https://pve1.lan:8443"}
|
|
// Response: {"success": true, "token": "...", "pve_instance": "pve1"}
|
|
func (h *TemperatureProxyHandlers) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)
|
|
return
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
var req struct {
|
|
Hostname string `json:"hostname"`
|
|
ProxyURL string `json:"proxy_url"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_json", "Failed to decode request body", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate inputs
|
|
hostname := strings.TrimSpace(req.Hostname)
|
|
proxyURL := strings.TrimSpace(req.ProxyURL)
|
|
|
|
if hostname == "" {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_hostname", "Hostname is required", nil)
|
|
return
|
|
}
|
|
|
|
if proxyURL == "" {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_proxy_url", "Proxy URL is required", nil)
|
|
return
|
|
}
|
|
|
|
// Validate proxy URL format
|
|
if !strings.HasPrefix(proxyURL, "https://") {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_proxy_url", "Proxy URL must use HTTPS", nil)
|
|
return
|
|
}
|
|
|
|
// Load current config
|
|
nodesConfig, err := h.persistence.LoadNodesConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "config_load_failed", "Failed to load configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Find matching PVE instance by hostname
|
|
var matchedInstance *config.PVEInstance
|
|
var matchedIndex int
|
|
|
|
for i := range nodesConfig.PVEInstances {
|
|
instance := &nodesConfig.PVEInstances[i]
|
|
|
|
// Try to match by hostname in the Host field or cluster endpoints
|
|
if strings.Contains(strings.ToLower(instance.Host), strings.ToLower(hostname)) {
|
|
matchedInstance = instance
|
|
matchedIndex = i
|
|
break
|
|
}
|
|
|
|
// Check cluster endpoints
|
|
if instance.IsCluster {
|
|
for _, ep := range instance.ClusterEndpoints {
|
|
if strings.EqualFold(ep.NodeName, hostname) || strings.Contains(strings.ToLower(ep.Host), strings.ToLower(hostname)) {
|
|
matchedInstance = instance
|
|
matchedIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if matchedInstance == nil {
|
|
writeErrorResponse(w, http.StatusNotFound, "pve_instance_not_found", fmt.Sprintf("No PVE instance configured for hostname '%s'. Add the instance in Pulse first.", hostname), nil)
|
|
return
|
|
}
|
|
|
|
// Generate a secure random token
|
|
token, err := generateSecureToken(32)
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "token_generation_failed", "Failed to generate authentication token", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update the instance with proxy configuration
|
|
nodesConfig.PVEInstances[matchedIndex].TemperatureProxyURL = proxyURL
|
|
nodesConfig.PVEInstances[matchedIndex].TemperatureProxyToken = token
|
|
|
|
// Save updated configuration
|
|
if err := h.persistence.SaveNodesConfig(nodesConfig.PVEInstances, nodesConfig.PBSInstances, nodesConfig.PMGInstances); err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "config_save_failed", "Failed to save configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
log.Info().
|
|
Str("hostname", hostname).
|
|
Str("proxy_url", proxyURL).
|
|
Str("pve_instance", matchedInstance.Name).
|
|
Msg("Temperature proxy registered successfully")
|
|
|
|
resp := map[string]any{
|
|
"success": true,
|
|
"token": token,
|
|
"pve_instance": matchedInstance.Name,
|
|
"message": fmt.Sprintf("Temperature proxy registered for instance '%s'", matchedInstance.Name),
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, resp); err != nil {
|
|
log.Error().Err(err).Msg("Failed to serialize temperature proxy registration response")
|
|
}
|
|
}
|
|
|
|
// HandleUnregister removes temperature proxy configuration from a PVE instance
|
|
//
|
|
// DELETE /api/temperature-proxy/unregister?hostname=pve1
|
|
func (h *TemperatureProxyHandlers) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only DELETE is allowed", nil)
|
|
return
|
|
}
|
|
|
|
hostname := strings.TrimSpace(r.URL.Query().Get("hostname"))
|
|
if hostname == "" {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_hostname", "Hostname query parameter is required", nil)
|
|
return
|
|
}
|
|
|
|
// Load current config
|
|
nodesConfig, err := h.persistence.LoadNodesConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "config_load_failed", "Failed to load configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Find matching PVE instance
|
|
var matchedIndex = -1
|
|
var matchedName string
|
|
|
|
for i := range nodesConfig.PVEInstances {
|
|
instance := &nodesConfig.PVEInstances[i]
|
|
|
|
if strings.Contains(strings.ToLower(instance.Host), strings.ToLower(hostname)) {
|
|
matchedIndex = i
|
|
matchedName = instance.Name
|
|
break
|
|
}
|
|
|
|
if instance.IsCluster {
|
|
for _, ep := range instance.ClusterEndpoints {
|
|
if strings.EqualFold(ep.NodeName, hostname) || strings.Contains(strings.ToLower(ep.Host), strings.ToLower(hostname)) {
|
|
matchedIndex = i
|
|
matchedName = instance.Name
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if matchedIndex == -1 {
|
|
writeErrorResponse(w, http.StatusNotFound, "pve_instance_not_found", fmt.Sprintf("No PVE instance found for hostname '%s'", hostname), nil)
|
|
return
|
|
}
|
|
|
|
// Clear proxy configuration
|
|
nodesConfig.PVEInstances[matchedIndex].TemperatureProxyURL = ""
|
|
nodesConfig.PVEInstances[matchedIndex].TemperatureProxyToken = ""
|
|
|
|
// Save updated configuration
|
|
if err := h.persistence.SaveNodesConfig(nodesConfig.PVEInstances, nodesConfig.PBSInstances, nodesConfig.PMGInstances); err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "config_save_failed", "Failed to save configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
log.Info().
|
|
Str("hostname", hostname).
|
|
Str("pve_instance", matchedName).
|
|
Msg("Temperature proxy unregistered")
|
|
|
|
resp := map[string]any{
|
|
"success": true,
|
|
"pve_instance": matchedName,
|
|
"message": "Temperature proxy configuration removed",
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, resp); err != nil {
|
|
log.Error().Err(err).Msg("Failed to serialize temperature proxy unregistration response")
|
|
}
|
|
}
|
|
|
|
// generateSecureToken creates a cryptographically secure random token
|
|
func generateSecureToken(byteLength int) (string, error) {
|
|
bytes := make([]byte, byteLength)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return base64.URLEncoding.EncodeToString(bytes), nil
|
|
}
|