feat: Agent zeigt Warnung wenn er nicht den Kunden-Key nutzt

- last_api_key Spalte in agents, wird bei Heartbeat gesetzt
- GetAgentsByCustomer liefert last_api_key mit
- Frontend: rot + Warnsymbol wenn Key abweicht vom Kunden-Agent-Key
- Grau wenn noch kein Heartbeat mit Key-Info
This commit is contained in:
cynfo3000 2026-03-10 00:23:59 +01:00
parent 20a976767b
commit 4023a14fe1
4 changed files with 40 additions and 13 deletions

View File

@ -252,6 +252,11 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion) h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion)
} }
// Genutzten API-Key speichern (aus Middleware-Kontext)
if keyInfo := GetAPIKeyFromContext(r); keyInfo != nil {
h.db.UpdateAgentAPIKey(req.AgentID, keyInfo.Key)
}
if err := h.db.SaveSystemData(req.AgentID, &req.SystemData); err != nil { if err := h.db.SaveSystemData(req.AgentID, &req.SystemData); err != nil {
if strings.Contains(err.Error(), "nicht gefunden") { if strings.Contains(err.Error(), "nicht gefunden") {
writeError(w, http.StatusNotFound, "Agent nicht gefunden") writeError(w, http.StatusNotFound, "Agent nicht gefunden")

View File

@ -140,6 +140,7 @@ func (d *Database) migrate() error {
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS agent_version TEXT NOT NULL DEFAULT ''") d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS agent_version TEXT NOT NULL DEFAULT ''")
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS update_requested BOOLEAN NOT NULL DEFAULT FALSE") d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS update_requested BOOLEAN NOT NULL DEFAULT FALSE")
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS platform TEXT NOT NULL DEFAULT 'freebsd'") d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS platform TEXT NOT NULL DEFAULT 'freebsd'")
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS last_api_key TEXT NOT NULL DEFAULT ''")
// Agent-Firmware Tabelle (Multi-Plattform) // Agent-Firmware Tabelle (Multi-Plattform)
d.db.Exec(`CREATE TABLE IF NOT EXISTS agent_firmware ( d.db.Exec(`CREATE TABLE IF NOT EXISTS agent_firmware (
@ -627,6 +628,10 @@ func (d *Database) UpdateAgentVersion(id, version string) error {
return err return err
} }
func (d *Database) UpdateAgentAPIKey(id, apiKey string) {
d.db.Exec("UPDATE agents SET last_api_key = $1 WHERE id = $2", apiKey, id)
}
// Update-Request Flag setzen/lesen/loeschen // Update-Request Flag setzen/lesen/loeschen
func (d *Database) SetUpdateRequest(id string, requested bool) error { func (d *Database) SetUpdateRequest(id string, requested bool) error {
_, err := d.db.Exec("UPDATE agents SET update_requested = $1 WHERE id = $2", requested, id) _, err := d.db.Exec("UPDATE agents SET update_requested = $1 WHERE id = $2", requested, id)
@ -1099,7 +1104,7 @@ func (d *Database) UnassignAgentCustomer(agentID string) error {
} }
func (d *Database) GetAgentsByCustomer(customerID int) ([]models.AgentResponse, error) { func (d *Database) GetAgentsByCustomer(customerID int) ([]models.AgentResponse, error) {
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id, agent_version FROM agents WHERE customer_id = $1 ORDER BY name", customerID) rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id, agent_version, last_api_key FROM agents WHERE customer_id = $1 ORDER BY name", customerID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1110,7 +1115,7 @@ func (d *Database) GetAgentsByCustomer(customerID int) ([]models.AgentResponse,
var a models.Agent var a models.Agent
var lastHB sql.NullTime var lastHB sql.NullTime
var custID sql.NullInt64 var custID sql.NullInt64
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID, &a.AgentVersion); err != nil { if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID, &a.AgentVersion, &a.LastAPIKey); err != nil {
return nil, err return nil, err
} }
if lastHB.Valid { if lastHB.Valid {

View File

@ -15,6 +15,7 @@ type Agent struct {
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"` LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
CustomerID *int `json:"customer_id,omitempty"` CustomerID *int `json:"customer_id,omitempty"`
UpdateRequested bool `json:"update_requested"` UpdateRequested bool `json:"update_requested"`
LastAPIKey string `json:"last_api_key,omitempty"`
} }
// AgentResponse erweitert Agent um den berechneten Status // AgentResponse erweitert Agent um den berechneten Status

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import api from '../api/client' import api from '../api/client'
import { copyToClipboard } from '../utils/clipboard' import { copyToClipboard } from '../utils/clipboard'
import { Plus, Trash2, Edit2, X, Check, ChevronDown, ChevronRight, Copy, Key, Monitor } from 'lucide-react' import { Plus, Trash2, Edit2, X, Check, ChevronDown, ChevronRight, Copy, Key, Monitor, AlertTriangle } from 'lucide-react'
const PERM_LABELS = { const PERM_LABELS = {
agent: { label: 'Agent', color: 'text-blue-400 bg-blue-900/30 border-blue-700/50' }, agent: { label: 'Agent', color: 'text-blue-400 bg-blue-900/30 border-blue-700/50' },
@ -232,16 +232,32 @@ function CustomerKeyPanel({ customerId }) {
{agents.length === 0 ? ( {agents.length === 0 ? (
<div className="text-xs text-gray-600">Keine Agents zugeordnet</div> <div className="text-xs text-gray-600">Keine Agents zugeordnet</div>
) : ( ) : (
agents.map((a) => ( agents.map((a) => {
<div key={a.id} className="flex items-center gap-2.5"> const wrongKey = agentKey && a.last_api_key && a.last_api_key !== agentKey.key
const unknownKey = agentKey && !a.last_api_key
return (
<div key={a.id} className={`flex items-center gap-2.5 rounded px-1.5 py-0.5 -mx-1.5 ${wrongKey ? 'bg-red-900/20' : ''}`}>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${ <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
a.status === 'online' ? 'bg-green-400' : a.status === 'online' ? 'bg-green-400' :
a.status === 'stale' ? 'bg-yellow-400' : 'bg-gray-600' a.status === 'stale' ? 'bg-yellow-400' : 'bg-gray-600'
}`} /> }`} />
<span className="text-xs text-gray-300 flex-1 min-w-0 truncate">{a.name}</span> <span className={`text-xs flex-1 min-w-0 truncate ${wrongKey ? 'text-red-300' : 'text-gray-300'}`}>
{a.name}
</span>
<span className="text-[10px] text-gray-600 font-mono flex-shrink-0">{a.agent_version || '—'}</span> <span className="text-[10px] text-gray-600 font-mono flex-shrink-0">{a.agent_version || '—'}</span>
{wrongKey && (
<span title={`Falscher Key: ${a.last_api_key?.substring(0,8)}...`}>
<AlertTriangle className="w-3 h-3 text-red-400 flex-shrink-0" />
</span>
)}
{unknownKey && (
<span title="Noch kein Heartbeat mit Key-Info">
<AlertTriangle className="w-3 h-3 text-gray-600 flex-shrink-0" />
</span>
)}
</div> </div>
)) )
})
)} )}
</div> </div>