feat: Extract Wild Central as standalone Go service
Extract the Central networking functionality from wild-cloud/api into a standalone service. Wild Central manages DNS (dnsmasq), gateway (HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot), security (CrowdSec), and DDNS — all the network appliance concerns. All Cloud-specific code (instances, clusters, nodes, apps, backups, operations, kubectl/talosctl tooling) has been removed. The API struct and route registration contain only Central endpoints. Tests updated to match the new API signature. Builds and all tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
internal/frontend/handler.go
Normal file
96
internal/frontend/handler.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Package frontend serves the Wild Cloud web UI.
|
||||
//
|
||||
// In development (WILD_CENTRAL_ENV=development), it reverse-proxies all
|
||||
// non-API requests to the Vite dev server for hot module replacement.
|
||||
//
|
||||
// In production, it serves pre-built static files from a directory
|
||||
// with SPA fallback (unknown paths serve index.html).
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handler returns an http.Handler that serves the web UI.
|
||||
//
|
||||
// In dev mode it proxies to the Vite dev server at viteURL (e.g. "http://localhost:5173").
|
||||
// In prod mode it serves static files from staticDir with SPA fallback.
|
||||
func Handler(staticDir, viteURL string) http.Handler {
|
||||
if isDev() && viteURL != "" {
|
||||
return devProxy(viteURL)
|
||||
}
|
||||
return prodHandler(staticDir)
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return os.Getenv("WILD_CENTRAL_ENV") == "development"
|
||||
}
|
||||
|
||||
// devProxy reverse-proxies everything to the Vite dev server,
|
||||
// including WebSocket upgrades for HMR (/__vite_hmr).
|
||||
func devProxy(rawURL string) http.Handler {
|
||||
target, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
slog.Error("invalid vite dev server URL", "url", rawURL, "error", err)
|
||||
return http.NotFoundHandler()
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
// Preserve the original Director, rewrite Host for Vite's allowedHosts check,
|
||||
// and forward WebSocket upgrade headers for HMR.
|
||||
defaultDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
defaultDirector(req)
|
||||
req.Host = target.Host
|
||||
if strings.EqualFold(req.Header.Get("Upgrade"), "websocket") {
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("frontend dev proxy active", "target", rawURL)
|
||||
return proxy
|
||||
}
|
||||
|
||||
// prodHandler serves static files with SPA fallback.
|
||||
// If the requested path matches a real file, serve it.
|
||||
// Otherwise serve index.html so client-side routing works.
|
||||
func prodHandler(dir string) http.Handler {
|
||||
slog.Info("frontend serving static files", "dir", dir)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Clean the path and try to find the file.
|
||||
p := filepath.Join(dir, filepath.Clean(r.URL.Path))
|
||||
|
||||
// Check if the file exists and is not a directory.
|
||||
fi, err := os.Stat(p)
|
||||
if err == nil && !fi.IsDir() {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for index.html inside directories.
|
||||
if err == nil && fi.IsDir() {
|
||||
index := filepath.Join(p, "index.html")
|
||||
if _, err := os.Stat(index); err == nil {
|
||||
http.ServeFile(w, r, index)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SPA fallback: serve root index.html for unmatched paths.
|
||||
index := filepath.Join(dir, "index.html")
|
||||
if _, err := os.Stat(index); errors.Is(err, fs.ErrNotExist) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, index)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user