Hub v0.6.0: Geo-restriction display + disable button + UUID cleanup
- Add geo-restriction section to customer detail page (status, countries, per-app overrides, sync state, errors) - Add "Összes geo-korlátozás eltávolítása" button that directly calls Cloudflare API to delete [felhom-geo] WAF rules (bypasses blocked tunnel) - Background retry to notify controller to disable geo in settings - New internal/cloudflare/unblock.go — minimal CF client for rule deletion - Remove legacy Monitoring UUIDs from config form, buildConfigJSON, handlePullConfig, volatileKeys, and controller.yaml.default Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+106
-35
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cfClient "gitea.dooplex.hu/admin/felhom-hub/internal/cloudflare"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/configgen"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -725,20 +726,6 @@ func buildConfigJSON(r *http.Request) string {
|
||||
overrides["git"] = git
|
||||
}
|
||||
|
||||
// Monitoring UUIDs (legacy — only written if user explicitly provides values)
|
||||
uuids := make(map[string]interface{})
|
||||
for _, key := range []string{"heartbeat", "system_health", "db_dump", "backup", "backup_integrity"} {
|
||||
if v := strings.TrimSpace(r.FormValue("uuid_" + key)); v != "" {
|
||||
uuids[key] = v
|
||||
}
|
||||
}
|
||||
if len(uuids) > 0 {
|
||||
if _, ok := overrides["monitoring"]; !ok {
|
||||
overrides["monitoring"] = make(map[string]interface{})
|
||||
}
|
||||
overrides["monitoring"].(map[string]interface{})["ping_uuids"] = uuids
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(overrides)
|
||||
return string(data)
|
||||
}
|
||||
@@ -762,12 +749,7 @@ func extractControllerYAML(infraData []byte) string {
|
||||
|
||||
// volatileKeys are YAML keys ignored during config comparison (always differ or deprecated).
|
||||
var volatileKeys = map[string]bool{
|
||||
"web.session_secret": true,
|
||||
"monitoring.ping_uuids.heartbeat": true,
|
||||
"monitoring.ping_uuids.system_health": true,
|
||||
"monitoring.ping_uuids.db_dump": true,
|
||||
"monitoring.ping_uuids.backup": true,
|
||||
"monitoring.ping_uuids.backup_integrity": true,
|
||||
"web.session_secret": true,
|
||||
}
|
||||
|
||||
// sensitiveKeyParts are substrings that indicate a value should be masked in diff output.
|
||||
@@ -1074,21 +1056,6 @@ func (s *Server) handlePullConfig(w http.ResponseWriter, r *http.Request, custom
|
||||
}
|
||||
}
|
||||
|
||||
// Monitoring UUIDs (legacy but still imported)
|
||||
if monitoring, ok := parsed["monitoring"].(map[string]interface{}); ok {
|
||||
if uuids, ok := monitoring["ping_uuids"].(map[string]interface{}); ok {
|
||||
uuidOverrides := make(map[string]interface{})
|
||||
for _, key := range []string{"heartbeat", "system_health", "db_dump", "backup", "backup_integrity"} {
|
||||
if v, ok := uuids[key].(string); ok && v != "" {
|
||||
uuidOverrides[key] = v
|
||||
}
|
||||
}
|
||||
if len(uuidOverrides) > 0 {
|
||||
overrides["monitoring"] = map[string]interface{}{"ping_uuids": uuidOverrides}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configJSON, _ := json.Marshal(overrides)
|
||||
cfg.ConfigJSON = string(configJSON)
|
||||
|
||||
@@ -1103,3 +1070,107 @@ func (s *Server) handlePullConfig(w http.ResponseWriter, r *http.Request, custom
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Config imported from controller"})
|
||||
}
|
||||
|
||||
// handleGeoDisable removes all [felhom-geo] WAF rules from Cloudflare for a customer,
|
||||
// and notifies the controller to disable geo-restriction in its settings.
|
||||
func (s *Server) handleGeoDisable(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
cfg, err := s.store.GetCustomerConfig(customerID)
|
||||
if err != nil || cfg == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Customer not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract CF API token from config_json → infrastructure.cf_api_token
|
||||
var overrides map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(cfg.ConfigJSON), &overrides); err != nil {
|
||||
overrides = make(map[string]interface{})
|
||||
}
|
||||
var cfToken string
|
||||
if infra, ok := overrides["infrastructure"].(map[string]interface{}); ok {
|
||||
cfToken, _ = infra["cf_api_token"].(string)
|
||||
}
|
||||
if cfToken == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No Cloudflare API token configured for this customer"})
|
||||
return
|
||||
}
|
||||
if cfg.Domain == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No domain configured for this customer"})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Remove WAF rules directly via Cloudflare API
|
||||
if err := cfClient.RemoveGeoRules(cfToken, cfg.Domain, s.logger); err != nil {
|
||||
s.logger.Printf("[ERROR] Geo disable for %s: Cloudflare removal failed: %v", customerID, err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Cloudflare API error: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Geo disable for %s: Cloudflare WAF rules removed", customerID)
|
||||
|
||||
// 2. Background: notify controller to disable geo in settings (retry for up to 10 min)
|
||||
customer, _ := s.store.GetCustomer(customerID)
|
||||
controllerURL := ""
|
||||
if customer != nil {
|
||||
controllerURL = customer.ControllerURL
|
||||
}
|
||||
if controllerURL == "" {
|
||||
var rpt struct {
|
||||
ControllerURL string `json:"controller_url"`
|
||||
}
|
||||
if customer != nil {
|
||||
json.Unmarshal([]byte(customer.ReportJSON), &rpt)
|
||||
controllerURL = rpt.ControllerURL
|
||||
}
|
||||
}
|
||||
|
||||
if controllerURL != "" && s.apiKey != "" {
|
||||
go s.notifyControllerGeoDisable(customerID, controllerURL)
|
||||
} else {
|
||||
s.logger.Printf("[WARN] Geo disable for %s: cannot notify controller (no URL or API key)", customerID)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Geo-restriction removed from Cloudflare. Controller will be notified."})
|
||||
}
|
||||
|
||||
// notifyControllerGeoDisable retries sending geo-disable to the controller every 30s for up to 10 min.
|
||||
func (s *Server) notifyControllerGeoDisable(customerID, controllerURL string) {
|
||||
geoURL := controllerURL + "/api/geo/settings"
|
||||
|
||||
for attempt := 1; attempt <= 20; attempt++ {
|
||||
req, err := http.NewRequest("POST", geoURL, strings.NewReader(`{"enabled":false,"allowed_countries":["HU"]}`))
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Geo disable notify %s attempt %d: create request: %v", customerID, attempt, err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+s.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
s.logger.Printf("[WARN] Geo disable notify %s attempt %d: %v", customerID, attempt, err)
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
s.logger.Printf("[INFO] Geo disable notify %s: controller confirmed (attempt %d): %s", customerID, attempt, string(body))
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[WARN] Geo disable notify %s attempt %d: status %d: %s", customerID, attempt, resp.StatusCode, string(body))
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
|
||||
s.logger.Printf("[ERROR] Geo disable notify %s: gave up after 20 attempts", customerID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user