From f066dd7e991a93892904a569d8947de539ccbde0 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Thu, 9 Jul 2026 04:34:03 +0000 Subject: [PATCH] feat: Add Cloudflare token verification endpoint Add /api/v1/cloudflare/verify, /api/v1/cloudflare/zone endpoints so the Central UI's Cloudflare page can verify tokens and manage zone configuration. This was a missing endpoint from the extraction. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/api/v1/handlers.go | 5 + internal/api/v1/handlers_cloudflare.go | 129 +++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 internal/api/v1/handlers_cloudflare.go diff --git a/internal/api/v1/handlers.go b/internal/api/v1/handlers.go index 995ba34..3507d9a 100644 --- a/internal/api/v1/handlers.go +++ b/internal/api/v1/handlers.go @@ -212,6 +212,11 @@ func (api *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/crowdsec/bouncers", api.CrowdSecGetBouncers).Methods("GET") r.HandleFunc("/api/v1/crowdsec/bouncers/{name}", api.CrowdSecDeleteBouncer).Methods("DELETE") + // Cloudflare management + r.HandleFunc("/api/v1/cloudflare/verify", api.CloudflareVerify).Methods("GET") + r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareGetZoneID).Methods("GET") + r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareSetZoneID).Methods("POST") + // Service registration r.HandleFunc("/api/v1/services", api.ServicesListAll).Methods("GET") r.HandleFunc("/api/v1/services", api.ServicesRegister).Methods("POST") diff --git a/internal/api/v1/handlers_cloudflare.go b/internal/api/v1/handlers_cloudflare.go new file mode 100644 index 0000000..d9030ce --- /dev/null +++ b/internal/api/v1/handlers_cloudflare.go @@ -0,0 +1,129 @@ +package v1 + +import ( + "encoding/json" + "net/http" + "path/filepath" +) + +type cloudflareZone struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type cloudflareVerifyResponse struct { + TokenConfigured bool `json:"tokenConfigured"` + TokenValid bool `json:"tokenValid"` + TokenStatus string `json:"tokenStatus"` + Zones []cloudflareZone `json:"zones"` + Error string `json:"error,omitempty"` +} + +// CloudflareVerify checks whether the stored Cloudflare API token is valid +// and returns the zones it can access. +func (api *API) CloudflareVerify(w http.ResponseWriter, r *http.Request) { + token := api.getCloudflareToken() + if token == "" { + respondJSON(w, http.StatusOK, cloudflareVerifyResponse{ + TokenConfigured: false, + }) + return + } + + resp := cloudflareVerifyResponse{TokenConfigured: true} + + tokenStatus, err := verifyCloudflareToken(token) + if err != nil { + resp.Error = err.Error() + respondJSON(w, http.StatusOK, resp) + return + } + resp.TokenStatus = tokenStatus + resp.TokenValid = tokenStatus == "active" + + zones, err := listCloudflareZones(token) + if err != nil { + resp.Error = "Token valid but failed to list zones: " + err.Error() + respondJSON(w, http.StatusOK, resp) + return + } + resp.Zones = zones + + respondJSON(w, http.StatusOK, resp) +} + +// CloudflareGetZoneID returns the stored zone ID from secrets. +func (api *API) CloudflareGetZoneID(w http.ResponseWriter, r *http.Request) { + zoneID := api.getCloudflareZoneID() + respondJSON(w, http.StatusOK, map[string]any{ + "zoneId": zoneID, + "configured": zoneID != "", + }) +} + +// CloudflareSetZoneID stores the zone ID in global secrets. +func (api *API) CloudflareSetZoneID(w http.ResponseWriter, r *http.Request) { + var req struct { + ZoneID string `json:"zoneId"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + secretsPath := filepath.Join(api.dataDir, "secrets.yaml") + if err := api.secrets.SetSecret(secretsPath, "cloudflare.zoneId", req.ZoneID); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save zone ID") + return + } + + respondMessage(w, http.StatusOK, "Zone ID saved") +} + +func verifyCloudflareToken(apiToken string) (string, error) { + req, err := http.NewRequest(http.MethodGet, + "https://api.cloudflare.com/client/v4/user/tokens/verify", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+apiToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var result struct { + Result struct { + Status string `json:"status"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", err + } + return result.Result.Status, nil +} + +func listCloudflareZones(apiToken string) ([]cloudflareZone, error) { + req, err := http.NewRequest(http.MethodGet, + "https://api.cloudflare.com/client/v4/zones", nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+apiToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Result []cloudflareZone `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result.Result, nil +}