Show routes, TLS certs, and port-forwarding on services page

- Add Routes model to services UI (paths, headers, IP whitelisting per route)
- Show TLS cert info per service with inline provision/renew actions
- Remove TLS Certificates section from dashboard (now on services page)
- Make gateway router port list dynamic from config + VPN state
- Add TODO for header validation in HAProxy config generation
This commit is contained in:
2026-07-10 06:10:40 +00:00
parent e78a1d548a
commit 68d6fde80d
9 changed files with 570 additions and 1072 deletions

View File

@@ -68,7 +68,7 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
version: version,
allowedOrigins: allowedOrigins,
config: configMgr,
secrets: secrets.NewManager(),
secrets: secrets.NewManager(filepath.Join(dataDir, "secrets.yaml")),
dnsmasq: dnsmasq.NewConfigGenerator(dnsmasqConfigPath),
haproxy: haproxy.NewManager(haproxyConfigPath),
nftables: nftables.NewManager(nftablesRulesPath),
@@ -327,26 +327,12 @@ func (api *API) UpdateGlobalConfig(w http.ResponseWriter, r *http.Request) {
// GetGlobalSecrets returns the global secrets (redacted by default)
func (api *API) GetGlobalSecrets(w http.ResponseWriter, r *http.Request) {
secretsPath := filepath.Join(api.dataDir, "secrets.yaml")
data, err := os.ReadFile(secretsPath)
secretsMap, err := api.secrets.GetAll()
if err != nil {
if os.IsNotExist(err) {
respondJSON(w, http.StatusOK, map[string]any{})
return
}
respondError(w, http.StatusInternalServerError, "Failed to read secrets")
return
}
var secretsMap map[string]any
if err := yaml.Unmarshal(data, &secretsMap); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to parse secrets")
return
}
if secretsMap == nil {
secretsMap = map[string]any{}
}
showRaw := r.URL.Query().Get("raw") == "true"
if !showRaw {
redactSecrets(secretsMap)
@@ -357,8 +343,6 @@ func (api *API) GetGlobalSecrets(w http.ResponseWriter, r *http.Request) {
// UpdateGlobalSecrets updates the global secrets
func (api *API) UpdateGlobalSecrets(w http.ResponseWriter, r *http.Request) {
secretsPath := filepath.Join(api.dataDir, "secrets.yaml")
body, err := io.ReadAll(r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to read request body")
@@ -371,36 +355,7 @@ func (api *API) UpdateGlobalSecrets(w http.ResponseWriter, r *http.Request) {
return
}
existingContent, err := storage.ReadFile(secretsPath)
if err != nil && !os.IsNotExist(err) {
respondError(w, http.StatusInternalServerError, "Failed to read existing secrets")
return
}
var existing map[string]any
if len(existingContent) > 0 {
if err := yaml.Unmarshal(existingContent, &existing); err != nil {
respondError(w, http.StatusBadRequest, "Failed to parse existing secrets")
return
}
} else {
existing = make(map[string]any)
}
for key, value := range updates {
existing[key] = value
}
yamlContent, err := yaml.Marshal(existing)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to marshal YAML")
return
}
lockPath := secretsPath + ".lock"
if err := storage.WithLock(lockPath, func() error {
return storage.WriteFile(secretsPath, yamlContent, 0600)
}); err != nil {
if err := api.secrets.MergeUpdate(updates); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to write secrets")
return
}
@@ -488,45 +443,19 @@ func redactSecrets(m map[string]any) {
// getCloudflareToken reads the Cloudflare API token from global secrets
func (api *API) getCloudflareToken() string {
secretsPath := filepath.Join(api.dataDir, "secrets.yaml")
data, err := os.ReadFile(secretsPath)
token, err := api.secrets.GetSecret("cloudflare.apiToken")
if err != nil {
return ""
}
var secretsMap map[string]any
if err := yaml.Unmarshal(data, &secretsMap); err != nil {
return ""
}
cloudflare, ok := secretsMap["cloudflare"].(map[string]any)
if !ok {
return ""
}
token, _ := cloudflare["apiToken"].(string)
return token
}
// getCloudflareZoneID reads the Cloudflare Zone ID from global secrets
func (api *API) getCloudflareZoneID() string {
secretsPath := filepath.Join(api.dataDir, "secrets.yaml")
data, err := os.ReadFile(secretsPath)
zoneID, err := api.secrets.GetSecret("cloudflare.zoneId")
if err != nil {
return ""
}
var secretsMap map[string]any
if err := yaml.Unmarshal(data, &secretsMap); err != nil {
return ""
}
cloudflare, ok := secretsMap["cloudflare"].(map[string]any)
if !ok {
return ""
}
zoneID, _ := cloudflare["zoneId"].(string)
return zoneID
}