diff --git a/internal/api/v1/handlers.go b/internal/api/v1/handlers.go index 3ff2237..dd83788 100644 --- a/internal/api/v1/handlers.go +++ b/internal/api/v1/handlers.go @@ -1,7 +1,6 @@ package v1 import ( - "errors" "fmt" "io" "log/slog" @@ -28,7 +27,6 @@ import ( "github.com/wild-cloud/wild-central/internal/secrets" "github.com/wild-cloud/wild-central/internal/domains" "github.com/wild-cloud/wild-central/internal/sse" - "github.com/wild-cloud/wild-central/internal/storage" "github.com/wild-cloud/wild-central/internal/wireguard" ) @@ -171,9 +169,11 @@ func (api *API) StartCentralStatusBroadcaster(startTime time.Time) { func (api *API) RegisterRoutes(r *mux.Router) { r.Use(RequestLoggingMiddleware) - // Global State (runtime settings — operator, DDNS, DHCP, etc.) - r.HandleFunc("/api/v1/state", api.GetState).Methods("GET") - r.HandleFunc("/api/v1/state", api.UpdateState).Methods("PUT") + // Resource-oriented settings (persisted in state.yaml) + r.HandleFunc("/api/v1/operator", api.GetOperator).Methods("GET") + r.HandleFunc("/api/v1/operator", api.UpdateOperator).Methods("PUT") + r.HandleFunc("/api/v1/central-domain", api.GetCentralDomain).Methods("GET") + r.HandleFunc("/api/v1/central-domain", api.UpdateCentralDomain).Methods("PUT") // Global Secrets r.HandleFunc("/api/v1/secrets", api.GetGlobalSecrets).Methods("GET") @@ -186,28 +186,44 @@ func (api *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/network/info", api.NetworkInfoHandler).Methods("GET") r.HandleFunc("/api/v1/network/resolve", api.NetworkResolveHandler).Methods("GET") - // dnsmasq management + // DNS (dnsmasq daemon) + r.HandleFunc("/api/v1/dns/settings", api.GetDNSSettings).Methods("GET") + r.HandleFunc("/api/v1/dns/settings", api.UpdateDNSSettings).Methods("PUT") r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET") r.HandleFunc("/api/v1/dnsmasq/config", api.DnsmasqGetConfig).Methods("GET") r.HandleFunc("/api/v1/dnsmasq/config", api.DnsmasqWriteConfig).Methods("PUT") r.HandleFunc("/api/v1/dnsmasq/restart", api.DnsmasqRestart).Methods("POST") r.HandleFunc("/api/v1/dnsmasq/generate", api.DnsmasqGenerate).Methods("POST") - r.HandleFunc("/api/v1/dnsmasq/dhcp/leases", api.DnsmasqDHCPLeases).Methods("GET") - r.HandleFunc("/api/v1/dnsmasq/dhcp/static", api.DnsmasqDHCPAddStatic).Methods("POST") - r.HandleFunc("/api/v1/dnsmasq/dhcp/static/{mac}", api.DnsmasqDHCPDeleteStatic).Methods("DELETE") - // HAProxy ingress management + // DHCP (feature, backed by dnsmasq) + r.HandleFunc("/api/v1/dhcp/config", api.GetDHCPConfig).Methods("GET") + r.HandleFunc("/api/v1/dhcp/config", api.UpdateDHCPConfig).Methods("PUT") + r.HandleFunc("/api/v1/dhcp/leases", api.DnsmasqDHCPLeases).Methods("GET") + r.HandleFunc("/api/v1/dhcp/static", api.DnsmasqDHCPAddStatic).Methods("POST") + r.HandleFunc("/api/v1/dhcp/static/{mac}", api.DnsmasqDHCPDeleteStatic).Methods("DELETE") + + // HAProxy gateway management r.HandleFunc("/api/v1/haproxy/status", api.HaproxyStatus).Methods("GET") r.HandleFunc("/api/v1/haproxy/config", api.HaproxyGetConfig).Methods("GET") r.HandleFunc("/api/v1/haproxy/generate", api.HaproxyGenerate).Methods("POST") r.HandleFunc("/api/v1/haproxy/restart", api.HaproxyRestart).Methods("POST") r.HandleFunc("/api/v1/haproxy/stats", api.HaproxyStats).Methods("GET") + r.HandleFunc("/api/v1/haproxy/routes", api.GetHAProxyRoutes).Methods("GET") + r.HandleFunc("/api/v1/haproxy/routes", api.UpdateHAProxyRoutes).Methods("PUT") // nftables firewall management r.HandleFunc("/api/v1/nftables/status", api.NftablesStatus).Methods("GET") + r.HandleFunc("/api/v1/nftables/config", api.GetNftablesConfig).Methods("GET") + r.HandleFunc("/api/v1/nftables/config", api.UpdateNftablesConfig).Methods("PUT") r.HandleFunc("/api/v1/nftables/apply", api.NftablesApply).Methods("POST") r.HandleFunc("/api/v1/nftables/interfaces", api.NftablesGetInterfaces).Methods("GET") + // DDNS management + r.HandleFunc("/api/v1/ddns/config", api.GetDDNSConfig).Methods("GET") + r.HandleFunc("/api/v1/ddns/config", api.UpdateDDNSConfig).Methods("PUT") + r.HandleFunc("/api/v1/ddns/status", api.DDNSStatus).Methods("GET") + r.HandleFunc("/api/v1/ddns/trigger", api.DDNSTrigger).Methods("POST") + // WireGuard VPN management r.HandleFunc("/api/v1/vpn/status", api.VpnStatus).Methods("GET") r.HandleFunc("/api/v1/vpn/config", api.VpnGetConfig).Methods("GET") @@ -260,89 +276,7 @@ func (api *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/events", api.GlobalEventStream).Methods("GET") } -// --- Global Config/Secrets handlers --- - -// GetState returns the global state wrapped in { configured, state }. -func (api *API) GetState(w http.ResponseWriter, r *http.Request) { - data, err := os.ReadFile(api.statePath()) - if err != nil { - respondJSON(w, http.StatusOK, map[string]any{ - "configured": false, - }) - return - } - - var stateMap map[string]any - if err := yaml.Unmarshal(data, &stateMap); err != nil { - respondError(w, http.StatusInternalServerError, "Failed to parse state") - return - } - - respondJSON(w, http.StatusOK, map[string]any{ - "configured": true, - "state": stateMap, - }) -} - -// UpdateState updates the global state -func (api *API) UpdateState(w http.ResponseWriter, r *http.Request) { - configPath := api.statePath() - - body, err := io.ReadAll(r.Body) - if err != nil { - respondError(w, http.StatusBadRequest, "Failed to read request body") - return - } - - var updates map[string]any - if err := yaml.Unmarshal(body, &updates); err != nil { - respondError(w, http.StatusBadRequest, "Invalid YAML format") - return - } - - // Read existing state (may not exist yet) - existingContent, err := storage.ReadFile(configPath) - if err != nil && !errors.Is(err, os.ErrNotExist) { - respondError(w, http.StatusInternalServerError, "Failed to read existing state") - return - } - - var existingConfig map[string]any - if len(existingContent) > 0 { - if err := yaml.Unmarshal(existingContent, &existingConfig); err != nil { - respondError(w, http.StatusBadRequest, "Failed to parse existing config") - return - } - } else { - existingConfig = make(map[string]any) - } - - for key, value := range updates { - existingConfig[key] = value - } - - yamlContent, err := yaml.Marshal(existingConfig) - if err != nil { - respondError(w, http.StatusInternalServerError, "Failed to marshal YAML") - return - } - - lockPath := configPath + ".lock" - if err := storage.WithLock(lockPath, func() error { - return storage.WriteFile(configPath, yamlContent, 0644) - }); err != nil { - respondError(w, http.StatusInternalServerError, "Failed to write config") - return - } - - slog.Info("global config updated") - - // Reload DDNS and re-register Central's service if config changed - go api.reloadDDNSIfEnabled() - go api.EnsureCentralDomain() - - respondMessage(w, http.StatusOK, "Config updated successfully") -} +// --- Secrets handlers --- // GetGlobalSecrets returns the global secrets (redacted by default) func (api *API) GetGlobalSecrets(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/v1/handlers_config_test.go b/internal/api/v1/handlers_config_test.go index e068850..c9209eb 100644 --- a/internal/api/v1/handlers_config_test.go +++ b/internal/api/v1/handlers_config_test.go @@ -9,31 +9,22 @@ import ( "path/filepath" "testing" - "github.com/wild-cloud/wild-central/internal/storage" "gopkg.in/yaml.v3" ) -func TestGetState_ReturnsWrappedResponse(t *testing.T) { +func TestGetOperator(t *testing.T) { api, tmpDir := setupTestAPI(t) - // Write a config with a central domain - cfg := map[string]any{ - "cloud": map[string]any{ - "central": map[string]any{ - "domain": "central.example.com", - }, - }, - "operator": map[string]any{ - "email": "test@example.com", - }, + // Write state with operator email + state := map[string]any{ + "operator": map[string]any{"email": "test@example.com"}, } - data, _ := yaml.Marshal(cfg) - storage.WriteFile(filepath.Join(tmpDir, "state.yaml"), data, 0644) + data, _ := yaml.Marshal(state) + os.WriteFile(filepath.Join(tmpDir, "state.yaml"), data, 0644) - req := httptest.NewRequest("GET", "/api/v1/state", nil) + req := httptest.NewRequest("GET", "/api/v1/operator", nil) w := httptest.NewRecorder() - - api.GetState(w, req) + api.GetOperator(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) @@ -41,34 +32,47 @@ func TestGetState_ReturnsWrappedResponse(t *testing.T) { var resp map[string]any json.Unmarshal(w.Body.Bytes(), &resp) - - // Must have "configured" and "state" keys - if resp["configured"] != true { - t.Errorf("expected configured=true, got %v", resp["configured"]) - } - state, ok := resp["state"].(map[string]any) - if !ok { - t.Fatalf("expected state to be a map, got %T", resp["state"]) - } - - // Verify nested structure is preserved - cloud, _ := state["cloud"].(map[string]any) - central, _ := cloud["central"].(map[string]any) - if central["domain"] != "central.example.com" { - t.Errorf("expected domain central.example.com, got %v", central["domain"]) + if resp["email"] != "test@example.com" { + t.Errorf("expected email test@example.com, got %v", resp["email"]) } } -func TestGetState_EmptyReturnsNotConfigured(t *testing.T) { +func TestUpdateOperator(t *testing.T) { api, tmpDir := setupTestAPI(t) - // Ensure no state file exists - os.Remove(filepath.Join(tmpDir, "state.yaml")) - - req := httptest.NewRequest("GET", "/api/v1/state", nil) + body, _ := json.Marshal(map[string]string{"email": "new@example.com"}) + req := httptest.NewRequest("PUT", "/api/v1/operator", bytes.NewReader(body)) w := httptest.NewRecorder() + api.UpdateOperator(w, req) - api.GetState(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + // Verify persisted + data, _ := os.ReadFile(filepath.Join(tmpDir, "state.yaml")) + var state map[string]any + yaml.Unmarshal(data, &state) + op := state["operator"].(map[string]any) + if op["email"] != "new@example.com" { + t.Errorf("expected email new@example.com, got %v", op["email"]) + } +} + +func TestGetCentralDomain(t *testing.T) { + api, tmpDir := setupTestAPI(t) + + state := map[string]any{ + "cloud": map[string]any{ + "central": map[string]any{"domain": "central.example.com"}, + }, + } + data, _ := yaml.Marshal(state) + os.WriteFile(filepath.Join(tmpDir, "state.yaml"), data, 0644) + + req := httptest.NewRequest("GET", "/api/v1/central-domain", nil) + w := httptest.NewRecorder() + api.GetCentralDomain(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) @@ -76,9 +80,27 @@ func TestGetState_EmptyReturnsNotConfigured(t *testing.T) { var resp map[string]any json.Unmarshal(w.Body.Bytes(), &resp) + if resp["domain"] != "central.example.com" { + t.Errorf("expected domain central.example.com, got %v", resp["domain"]) + } +} - if resp["configured"] != false { - t.Errorf("expected configured=false, got %v", resp["configured"]) +func TestUpdateCentralDomain(t *testing.T) { + api, _ := setupTestAPI(t) + + body, _ := json.Marshal(map[string]string{"domain": "new.example.com"}) + req := httptest.NewRequest("PUT", "/api/v1/central-domain", bytes.NewReader(body)) + w := httptest.NewRecorder() + api.UpdateCentralDomain(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]any + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["domain"] != "new.example.com" { + t.Errorf("expected domain new.example.com, got %v", resp["domain"]) } } @@ -103,12 +125,10 @@ func TestGetGlobalSecrets_RedactsLeafValues(t *testing.T) { var resp map[string]any json.Unmarshal(w.Body.Bytes(), &resp) - // Top-level leaf should be redacted if resp["topLevelSecret"] != "********" { t.Errorf("expected topLevelSecret redacted, got %v", resp["topLevelSecret"]) } - // Nested map structure should be preserved cf, ok := resp["cloudflare"].(map[string]any) if !ok { t.Fatalf("expected cloudflare to be a map, got %T", resp["cloudflare"]) @@ -116,9 +136,6 @@ func TestGetGlobalSecrets_RedactsLeafValues(t *testing.T) { if cf["apiToken"] != "********" { t.Errorf("expected apiToken redacted, got %v", cf["apiToken"]) } - if cf["zoneId"] != "********" { - t.Errorf("expected zoneId redacted, got %v", cf["zoneId"]) - } } func TestGetGlobalSecrets_RawShowsValues(t *testing.T) { @@ -146,39 +163,6 @@ func TestGetGlobalSecrets_RawShowsValues(t *testing.T) { } } -func TestUpdateState(t *testing.T) { - api, tmpDir := setupTestAPI(t) - - update := map[string]any{ - "cloud": map[string]any{ - "central": map[string]any{ - "domain": "new.example.com", - }, - }, - } - body, _ := json.Marshal(update) - - req := httptest.NewRequest("PUT", "/api/v1/state", bytes.NewReader(body)) - w := httptest.NewRecorder() - - api.UpdateState(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - - // Verify file was written - data, _ := os.ReadFile(filepath.Join(tmpDir, "state.yaml")) - var cfg map[string]any - yaml.Unmarshal(data, &cfg) - - cloud := cfg["cloud"].(map[string]any) - central := cloud["central"].(map[string]any) - if central["domain"] != "new.example.com" { - t.Errorf("expected domain new.example.com, got %v", central["domain"]) - } -} - func TestCloudflareVerify_NoToken(t *testing.T) { api, _ := setupTestAPI(t) diff --git a/internal/api/v1/handlers_settings.go b/internal/api/v1/handlers_settings.go new file mode 100644 index 0000000..16846a3 --- /dev/null +++ b/internal/api/v1/handlers_settings.go @@ -0,0 +1,267 @@ +package v1 + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/wild-cloud/wild-central/internal/config" +) + +// loadState loads state from disk, returning an empty state if the file doesn't exist. +func (api *API) loadState() *config.State { + state, err := config.LoadState(api.statePath()) + if err != nil { + return &config.State{} + } + return state +} + +// saveState writes state to disk and returns an error suitable for HTTP responses. +func (api *API) saveState(state *config.State) error { + return config.SaveState(state, api.statePath()) +} + +// --- Operator --- + +func (api *API) GetOperator(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, api.loadState().Operator) +} + +func (api *API) UpdateOperator(w http.ResponseWriter, r *http.Request) { + var updates struct { + Email string `json:"email"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + state.Operator.Email = updates.Email + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + slog.Info("operator updated", "email", updates.Email) + respondJSON(w, http.StatusOK, state.Operator) +} + +// --- Central Domain --- + +func (api *API) GetCentralDomain(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, map[string]string{ + "domain": api.loadState().Cloud.Central.Domain, + }) +} + +func (api *API) UpdateCentralDomain(w http.ResponseWriter, r *http.Request) { + var updates struct { + Domain string `json:"domain"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + state.Cloud.Central.Domain = updates.Domain + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + go api.EnsureCentralDomain() + + slog.Info("central domain updated", "domain", updates.Domain) + respondJSON(w, http.StatusOK, map[string]string{"domain": updates.Domain}) +} + +// --- DDNS Config --- + +func (api *API) GetDDNSConfig(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, api.loadState().Cloud.DDNS) +} + +func (api *API) UpdateDDNSConfig(w http.ResponseWriter, r *http.Request) { + var updates struct { + Enabled *bool `json:"enabled,omitempty"` + Provider string `json:"provider,omitempty"` + IntervalMinutes *int `json:"intervalMinutes,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + if updates.Enabled != nil { + state.Cloud.DDNS.Enabled = *updates.Enabled + } + if updates.Provider != "" { + state.Cloud.DDNS.Provider = updates.Provider + } + if updates.IntervalMinutes != nil { + state.Cloud.DDNS.IntervalMinutes = *updates.IntervalMinutes + } + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + go api.reloadDDNSIfEnabled() + + slog.Info("DDNS config updated", "enabled", state.Cloud.DDNS.Enabled) + respondJSON(w, http.StatusOK, state.Cloud.DDNS) +} + +// --- DNS Settings (dnsmasq IP/interface) --- + +func (api *API) GetDNSSettings(w http.ResponseWriter, r *http.Request) { + d := api.loadState().Cloud.Dnsmasq + respondJSON(w, http.StatusOK, map[string]string{ + "ip": d.IP, + "interface": d.Interface, + }) +} + +func (api *API) UpdateDNSSettings(w http.ResponseWriter, r *http.Request) { + var updates struct { + IP string `json:"ip,omitempty"` + Interface string `json:"interface,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + if updates.IP != "" { + state.Cloud.Dnsmasq.IP = updates.IP + } + if updates.Interface != "" { + state.Cloud.Dnsmasq.Interface = updates.Interface + } + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + slog.Info("DNS settings updated", "ip", state.Cloud.Dnsmasq.IP) + respondJSON(w, http.StatusOK, map[string]string{ + "ip": state.Cloud.Dnsmasq.IP, + "interface": state.Cloud.Dnsmasq.Interface, + }) +} + +// --- DHCP Config --- + +func (api *API) GetDHCPConfig(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, api.loadState().Cloud.Dnsmasq.DHCP) +} + +func (api *API) UpdateDHCPConfig(w http.ResponseWriter, r *http.Request) { + var updates struct { + Enabled *bool `json:"enabled,omitempty"` + RangeStart string `json:"rangeStart,omitempty"` + RangeEnd string `json:"rangeEnd,omitempty"` + LeaseTime string `json:"leaseTime,omitempty"` + Gateway string `json:"gateway,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + dhcp := &state.Cloud.Dnsmasq.DHCP + if updates.Enabled != nil { + dhcp.Enabled = *updates.Enabled + } + if updates.RangeStart != "" { + dhcp.RangeStart = updates.RangeStart + } + if updates.RangeEnd != "" { + dhcp.RangeEnd = updates.RangeEnd + } + if updates.LeaseTime != "" { + dhcp.LeaseTime = updates.LeaseTime + } + if updates.Gateway != "" { + dhcp.Gateway = updates.Gateway + } + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + slog.Info("DHCP config updated", "enabled", dhcp.Enabled) + respondJSON(w, http.StatusOK, state.Cloud.Dnsmasq.DHCP) +} + +// --- nftables Config --- + +func (api *API) GetNftablesConfig(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, api.loadState().Cloud.Nftables) +} + +func (api *API) UpdateNftablesConfig(w http.ResponseWriter, r *http.Request) { + var updates struct { + Enabled *bool `json:"enabled,omitempty"` + WANInterface string `json:"wanInterface,omitempty"` + ExtraPorts []config.ExtraPort `json:"extraPorts,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + if updates.Enabled != nil { + state.Cloud.Nftables.Enabled = updates.Enabled + } + if updates.WANInterface != "" { + state.Cloud.Nftables.WANInterface = updates.WANInterface + } + if updates.ExtraPorts != nil { + state.Cloud.Nftables.ExtraPorts = updates.ExtraPorts + } + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + slog.Info("nftables config updated") + respondJSON(w, http.StatusOK, state.Cloud.Nftables) +} + +// --- HAProxy Custom Routes --- + +func (api *API) GetHAProxyRoutes(w http.ResponseWriter, r *http.Request) { + respondJSON(w, http.StatusOK, map[string]any{ + "customRoutes": api.loadState().Cloud.HAProxy.CustomRoutes, + }) +} + +func (api *API) UpdateHAProxyRoutes(w http.ResponseWriter, r *http.Request) { + var updates struct { + CustomRoutes []config.HAProxyCustomRoute `json:"customRoutes"` + } + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + state := api.loadState() + state.Cloud.HAProxy.CustomRoutes = updates.CustomRoutes + if err := api.saveState(state); err != nil { + respondError(w, http.StatusInternalServerError, "Failed to save state") + return + } + + slog.Info("HAProxy custom routes updated", "count", len(updates.CustomRoutes)) + respondJSON(w, http.StatusOK, map[string]any{ + "customRoutes": state.Cloud.HAProxy.CustomRoutes, + }) +} diff --git a/web/src/components/DashboardComponent.tsx b/web/src/components/DashboardComponent.tsx index 5f7cb4f..65984fd 100644 --- a/web/src/components/DashboardComponent.tsx +++ b/web/src/components/DashboardComponent.tsx @@ -12,9 +12,8 @@ import { import { useCloudflare } from '../hooks/useCloudflare'; import { useDdns } from '../hooks/useDdns'; import { useCentralStatus } from '../hooks/useCentralStatus'; -import { useConfig } from '../hooks'; import { useVpn } from '../hooks/useVpn'; -import { secretsApi } from '../services/api'; +import { secretsApi, dnsSettingsApi, nftablesConfigApi } from '../services/api'; import { usePageHelp } from '../hooks/usePageHelp'; import type { DdnsRecordStatus } from '../services/api/networking'; @@ -22,8 +21,9 @@ export function DashboardComponent() { const { data: centralStatus, isLoading: statusLoading, restart: restartDaemon, isRestarting } = useCentralStatus(); const { verification, isLoading: cfLoading } = useCloudflare(); const { status: ddnsStatus, isLoadingStatus: ddnsLoading, trigger: triggerDdns, isTriggering } = useDdns(); - const { config: globalConfig } = useConfig(); const { config: vpnConfig } = useVpn(); + const { data: dnsSettings } = useQuery({ queryKey: ['dns', 'settings'], queryFn: () => dnsSettingsApi.get() }); + const { data: nftConfig } = useQuery({ queryKey: ['nftables', 'config'], queryFn: () => nftablesConfigApi.get() }); const queryClient = useQueryClient(); const { data: globalSecrets } = useQuery({ @@ -379,9 +379,9 @@ export function DashboardComponent() {