- Agent: WAU registry config check (include/exclude URLs), pending updates count - Backend: /wau/compliance per customer, /wau/compliance/summary all customers - Frontend: compliance badge in customer list, compliance panel in WAU tab - config.js: no longer gitignored (secrets-free, reads from env/window.__RMM_CONFIG__) - vite.config.js: reads backend URL from .env instead of hardcoded value - .env.example added for clean setup
204 lines
6.6 KiB
Go
204 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// setupWAURoutes registriert alle WAU-Endpunkte
|
|
func (h *Handler) setupWAURoutes(mux *http.ServeMux) {
|
|
// Authentifizierte Endpunkte (API-Key oder JWT)
|
|
mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules)
|
|
mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule)
|
|
mux.HandleFunc("DELETE /api/v1/customers/{id}/wau/rules/{rid}", h.deleteWAURule)
|
|
mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory)
|
|
mux.HandleFunc("GET /api/v1/customers/{id}/wau/compliance", h.getWAUCompliance)
|
|
|
|
// Compliance-Summary für alle Kunden
|
|
mux.HandleFunc("GET /api/v1/wau/compliance/summary", h.getWAUComplianceSummary)
|
|
|
|
// Plaintext-Endpunkte für WAU (kein Auth nötig — enthält nur App-IDs)
|
|
mux.HandleFunc("GET /api/v1/customers/{id}/wau/included", h.wauIncludedList)
|
|
mux.HandleFunc("GET /api/v1/customers/{id}/wau/excluded", h.wauExcludedList)
|
|
}
|
|
|
|
// GET /api/v1/customers/{id}/wau/rules
|
|
func (h *Handler) listWAURules(w http.ResponseWriter, r *http.Request) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
|
return
|
|
}
|
|
rules, err := h.db.ListWAURules(customerID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rules)
|
|
}
|
|
|
|
// POST /api/v1/customers/{id}/wau/rules
|
|
func (h *Handler) addWAURule(w http.ResponseWriter, r *http.Request) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
WingetID string `json:"winget_id"`
|
|
DisplayName string `json:"display_name"`
|
|
RuleType string `json:"rule_type"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
req.WingetID = strings.TrimSpace(req.WingetID)
|
|
req.DisplayName = strings.TrimSpace(req.DisplayName)
|
|
|
|
if req.WingetID == "" {
|
|
writeError(w, http.StatusBadRequest, "winget_id erforderlich")
|
|
return
|
|
}
|
|
if req.RuleType != "allow" && req.RuleType != "block" {
|
|
writeError(w, http.StatusBadRequest, "rule_type muss 'allow' oder 'block' sein")
|
|
return
|
|
}
|
|
if req.DisplayName == "" {
|
|
req.DisplayName = req.WingetID
|
|
}
|
|
|
|
rule, err := h.db.AddWAURule(customerID, req.WingetID, req.DisplayName, req.RuleType)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Speichern")
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "wau.rule.add", "customer", strconv.Itoa(customerID), "",
|
|
req.RuleType+": "+req.WingetID)
|
|
writeJSON(w, http.StatusCreated, rule)
|
|
}
|
|
|
|
// DELETE /api/v1/customers/{id}/wau/rules/{rid}
|
|
func (h *Handler) deleteWAURule(w http.ResponseWriter, r *http.Request) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
|
return
|
|
}
|
|
ruleID, err := strconv.Atoi(r.PathValue("rid"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Regel-ID")
|
|
return
|
|
}
|
|
|
|
ok, err := h.db.DeleteWAURule(customerID, ruleID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Löschen")
|
|
return
|
|
}
|
|
if !ok {
|
|
writeError(w, http.StatusNotFound, "Regel nicht gefunden")
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "wau.rule.delete", "customer", strconv.Itoa(customerID), "", strconv.Itoa(ruleID))
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Regel gelöscht"})
|
|
}
|
|
|
|
// GET /api/v1/customers/{id}/wau/inventory
|
|
func (h *Handler) getWAUInventory(w http.ResponseWriter, r *http.Request) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
|
return
|
|
}
|
|
entries, err := h.db.GetWAUSoftwareInventory(customerID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden des Inventars")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, entries)
|
|
}
|
|
|
|
// GET /api/v1/customers/{id}/wau/included → Plaintext für WAU
|
|
func (h *Handler) wauIncludedList(w http.ResponseWriter, r *http.Request) {
|
|
h.serveWAUList(w, r, "allow")
|
|
}
|
|
|
|
// GET /api/v1/customers/{id}/wau/excluded → Plaintext für WAU
|
|
func (h *Handler) wauExcludedList(w http.ResponseWriter, r *http.Request) {
|
|
h.serveWAUList(w, r, "block")
|
|
}
|
|
|
|
func (h *Handler) serveWAUList(w http.ResponseWriter, r *http.Request, ruleType string) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Kunden-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
ids, err := h.db.GetWAUList(customerID, ruleType)
|
|
if err != nil {
|
|
http.Error(w, "Fehler", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(strings.Join(ids, "\r\n")))
|
|
if len(ids) > 0 {
|
|
w.Write([]byte("\r\n"))
|
|
}
|
|
}
|
|
|
|
// wauExpectedURLs leitet die erwarteten WAU-URLs aus dem Request-Host ab
|
|
func wauExpectedURLs(r *http.Request, customerID int) (includeURL, excludeURL string) {
|
|
scheme := "https"
|
|
if r.TLS == nil && (r.Header.Get("X-Forwarded-Proto") == "" || r.Header.Get("X-Forwarded-Proto") == "http") {
|
|
scheme = "http"
|
|
}
|
|
if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" {
|
|
scheme = fwd
|
|
}
|
|
host := r.Host
|
|
base := scheme + "://" + host
|
|
includeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/included"
|
|
excludeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/excluded"
|
|
return
|
|
}
|
|
|
|
// GET /api/v1/customers/{id}/wau/compliance
|
|
func (h *Handler) getWAUCompliance(w http.ResponseWriter, r *http.Request) {
|
|
customerID, err := strconv.Atoi(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
|
return
|
|
}
|
|
includeURL, excludeURL := wauExpectedURLs(r, customerID)
|
|
agents, err := h.db.GetWAUCompliance(customerID, includeURL, excludeURL)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"agents": agents,
|
|
"include_url": includeURL,
|
|
"exclude_url": excludeURL,
|
|
})
|
|
}
|
|
|
|
// GET /api/v1/wau/compliance/summary — Zusammenfassung für alle Kunden
|
|
func (h *Handler) getWAUComplianceSummary(w http.ResponseWriter, r *http.Request) {
|
|
// Ohne customer_id können wir keine URLs ableiten — wir geben Summary ohne URL-Check zurück
|
|
// oder nutzen customer_id=0 als Wildcard (kein URL-Check)
|
|
summaries, err := h.db.GetAllCustomersWAUSummary("", "")
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, summaries)
|
|
}
|