docs(reference): import Dune: Awakening server-manager references
Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.
- icehunter/ dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
closest analog to our agent's Dune docker control plane (compose
lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/ Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
Hyper-V self-host path + game-config schema
See docs/reference-repos/README.md for the full index + how we use each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
481
docs/reference-repos/icehunter/cmd/dune-admin/server.go
Normal file
481
docs/reference-repos/icehunter/cmd/dune-admin/server.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||
)
|
||||
|
||||
var allowedOrigins []string
|
||||
|
||||
func init() {
|
||||
raw := envOr("ALLOWED_ORIGINS", "https://dune-admin.layout.tools,http://localhost:5173")
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
if o = strings.TrimSpace(o); o != "" {
|
||||
allowedOrigins = append(allowedOrigins, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func originAllowed(origin string) bool {
|
||||
for _, o := range allowedOrigins {
|
||||
if o == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newDirectorProxy builds the /director/ reverse-proxy handler for target. It
|
||||
// strips the /director prefix before forwarding and routes upstream connections
|
||||
// through dial (the executor tunnel), so the director is reachable from
|
||||
// wherever the executor runs rather than the dune-admin host.
|
||||
func newDirectorProxy(target *url.URL, dial func(network, addr string) (net.Conn, error)) http.HandlerFunc {
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.Transport = httpTransportVia(dial)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/director")
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
r.Host = target.Host
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// originAllowedForRequest applies the explicit allowlist AND a same-host
|
||||
// exception: a browser requesting from `http://172.16.12.59:9090/` against the
|
||||
// dune-admin server running on the same host should not be considered cross-
|
||||
// origin and never needs to be added to ALLOWED_ORIGINS.
|
||||
//
|
||||
// When Origin is absent (non-browser WebSocket clients), the request is allowed
|
||||
// only if the TCP connection originates from a loopback address. r.RemoteAddr
|
||||
// is used — not r.Host, which is a client-controlled header and can be spoofed.
|
||||
func originAllowedForRequest(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
// No Origin header: allow only actual loopback TCP connections.
|
||||
remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(remoteHost)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
if u, err := url.Parse(origin); err == nil && u.Host == r.Host {
|
||||
return true
|
||||
}
|
||||
return originAllowed(origin)
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
w.Header().Set("Vary", "Origin")
|
||||
if origin != "" && originAllowedForRequest(r) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Private-Network", "true")
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func startServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// ── status ────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/status", handleStatus)
|
||||
mux.HandleFunc("POST /api/v1/reconnect", handleReconnect)
|
||||
mux.HandleFunc("GET /api/v1/config", handleGetConfig)
|
||||
mux.HandleFunc("POST /api/v1/config", handleSaveConfig)
|
||||
mux.HandleFunc("GET /api/v1/update/check", handleUpdateCheck)
|
||||
mux.HandleFunc("POST /api/v1/update/apply", handleUpdateApply)
|
||||
|
||||
// ── server settings (UserGame.ini / UserOverrides.ini) ────────────────
|
||||
mux.HandleFunc("GET /api/v1/server-settings", handleGetServerSettings)
|
||||
mux.HandleFunc("PUT /api/v1/server-settings", handleUpdateServerSettings)
|
||||
mux.HandleFunc("PUT /api/v1/server-settings/raw", handleUpdateRawSection)
|
||||
|
||||
// ── director config (Battlegroup Director / map persistence — AMP) ────────
|
||||
mux.HandleFunc("GET /api/v1/director-config", handleGetDirectorConfig)
|
||||
mux.HandleFunc("PUT /api/v1/director-config", handleUpdateDirectorConfig)
|
||||
|
||||
// ── scheduled restarts ────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/scheduled-restarts", handleGetScheduledRestarts)
|
||||
mux.HandleFunc("PUT /api/v1/scheduled-restarts", handleUpdateScheduledRestarts)
|
||||
mux.HandleFunc("POST /api/v1/scheduled-restarts/skip-next", handleSkipNextRestart)
|
||||
|
||||
// Database backups (#150) — AMP-native pg_dump/restore + scheduling.
|
||||
mux.HandleFunc("GET /api/v1/db-backups", handleDBBackupList)
|
||||
mux.HandleFunc("POST /api/v1/db-backups", handleDBBackupCreate)
|
||||
mux.HandleFunc("DELETE /api/v1/db-backups", handleDBBackupDelete)
|
||||
mux.HandleFunc("GET /api/v1/db-backups/download", handleDBBackupDownload)
|
||||
mux.HandleFunc("POST /api/v1/db-backups/restore", handleDBBackupRestore)
|
||||
mux.HandleFunc("GET /api/v1/scheduled-backups", handleGetScheduledBackups)
|
||||
mux.HandleFunc("PUT /api/v1/scheduled-backups", handleUpdateScheduledBackups)
|
||||
|
||||
// Web interfaces (#155) — operator-configurable links for the Server Health card.
|
||||
mux.HandleFunc("GET /api/v1/web-interfaces", handleGetWebInterfaces)
|
||||
mux.HandleFunc("PUT /api/v1/web-interfaces", handleUpdateWebInterfaces)
|
||||
|
||||
// ── battlegroup ───────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/battlegroup/status", handleBGStatus)
|
||||
mux.HandleFunc("POST /api/v1/battlegroup/exec", handleBGExec)
|
||||
mux.HandleFunc("GET /api/v1/battlegroup/pods", handleBGPods)
|
||||
mux.HandleFunc("GET /api/v1/battlegroup/backup-files", handleBGBackupFiles)
|
||||
mux.HandleFunc("GET /api/v1/battlegroup/backup-files/download", handleBGBackupDownload)
|
||||
mux.HandleFunc("POST /api/v1/battlegroup/backup-files/upload", handleBGBackupUpload)
|
||||
mux.HandleFunc("POST /api/v1/battlegroup/restore", handleBGRestore)
|
||||
|
||||
// ── players ───────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/players", handleGetPlayers)
|
||||
mux.HandleFunc("GET /api/v1/players/online", handleGetOnlineState)
|
||||
mux.HandleFunc("GET /api/v1/players/currency", handleGetCurrency)
|
||||
mux.HandleFunc("GET /api/v1/players/factions", handleGetFactions)
|
||||
mux.HandleFunc("GET /api/v1/players/specs", handleGetSpecs)
|
||||
mux.HandleFunc("GET /api/v1/players/summary", handleGetPlayerSummary)
|
||||
mux.HandleFunc("GET /api/v1/players/faction-trends", handleGetFactionTrends)
|
||||
mux.HandleFunc("GET /api/v1/players/templates", handleGetTemplates)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/inventory", handleGetInventory)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/journey", handleGetJourney)
|
||||
mux.HandleFunc("POST /api/v1/players/give-item", handleGiveItem)
|
||||
mux.HandleFunc("POST /api/v1/players/give-items", handleGiveItems)
|
||||
mux.HandleFunc("POST /api/v1/players/give-currency", handleGiveCurrency)
|
||||
mux.HandleFunc("POST /api/v1/players/grant-live", handleGrantLive)
|
||||
mux.HandleFunc("POST /api/v1/players/give-faction-rep", handleGiveFactionRep)
|
||||
mux.HandleFunc("POST /api/v1/players/give-scrip", handleGiveScrip)
|
||||
mux.HandleFunc("POST /api/v1/players/award-xp", handleAwardXP)
|
||||
mux.HandleFunc("POST /api/v1/players/award-char-xp", handleAwardCharXP)
|
||||
mux.HandleFunc("POST /api/v1/players/award-intel", handleAwardIntel)
|
||||
mux.HandleFunc("POST /api/v1/players/rename", handleRenameCharacter)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/tags", handleGetPlayerTags)
|
||||
mux.HandleFunc("POST /api/v1/players/update-tags", handleUpdatePlayerTags)
|
||||
mux.HandleFunc("POST /api/v1/players/returning-player-award", handleGrantReturningPlayerAward)
|
||||
mux.HandleFunc("POST /api/v1/players/dismiss-returning-player-award", handleDismissReturningPlayerAward)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/export", handleCharacterExport)
|
||||
mux.HandleFunc("POST /api/v1/players/delete-account", handleDeleteAccount)
|
||||
mux.HandleFunc("DELETE /api/v1/players/item/{id}", handleDeleteItem)
|
||||
mux.HandleFunc("POST /api/v1/players/reset-spec", handleResetSpec)
|
||||
mux.HandleFunc("POST /api/v1/players/set-faction-tier", handleSetFactionTier)
|
||||
mux.HandleFunc("POST /api/v1/players/progression-unlock", handleProgressionUnlock)
|
||||
mux.HandleFunc("POST /api/v1/players/progression-reverse", handleProgressionReverse)
|
||||
mux.HandleFunc("GET /api/v1/progression/presets", handleListProgressionPresets)
|
||||
mux.HandleFunc("POST /api/v1/players/progression/apply-preset", handleApplyProgressionPreset)
|
||||
mux.HandleFunc("POST /api/v1/players/journey/complete", handleJourneyComplete)
|
||||
mux.HandleFunc("POST /api/v1/players/journey/reset", handleJourneyReset)
|
||||
mux.HandleFunc("POST /api/v1/players/journey/wipe", handleJourneyWipe)
|
||||
mux.HandleFunc("POST /api/v1/players/contract/complete", handleCompleteContract)
|
||||
mux.HandleFunc("POST /api/v1/players/contracts/complete", handleCompleteContracts)
|
||||
mux.HandleFunc("POST /api/v1/players/contracts/reverse", handleReverseContracts)
|
||||
mux.HandleFunc("POST /api/v1/players/grant-job-skills", handleGrantJobSkills)
|
||||
mux.HandleFunc("POST /api/v1/players/reset-job-skills", handleResetJobSkills)
|
||||
mux.HandleFunc("POST /api/v1/players/set-starter-class", handleSetStarterClass)
|
||||
mux.HandleFunc("GET /api/v1/contracts", handleListContracts)
|
||||
mux.HandleFunc("POST /api/v1/players/delete-tutorials", handleDeleteTutorials)
|
||||
mux.HandleFunc("POST /api/v1/players/wipe-codex", handleWipeCodex)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/char-xp", handleGetCharXP)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/specs", handleGetPlayerSpecs)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/keystones", handleGetPlayerKeystones)
|
||||
mux.HandleFunc("POST /api/v1/players/grant-all-keystones", handleGrantAllKeystones)
|
||||
mux.HandleFunc("POST /api/v1/players/reset-all-keystones", handleResetAllKeystones)
|
||||
mux.HandleFunc("POST /api/v1/players/grant-max-spec", handleGrantMaxSpec)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/vehicles", handleGetPlayerVehicles)
|
||||
mux.HandleFunc("POST /api/v1/players/repair-item", handleRepairItem)
|
||||
mux.HandleFunc("POST /api/v1/players/repair-gear", handleRepairPlayerGear)
|
||||
mux.HandleFunc("POST /api/v1/players/repair-vehicle", handleRepairVehicle)
|
||||
mux.HandleFunc("POST /api/v1/players/refuel-vehicle", handleRefuelVehicle)
|
||||
mux.HandleFunc("GET /api/v1/players/partitions", handleGetPartitions)
|
||||
mux.HandleFunc("POST /api/v1/players/teleport", handleTeleportPlayer)
|
||||
mux.HandleFunc("POST /api/v1/players/teleport-coords", handleTeleportCoords)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/position", handleGetPlayerPosition)
|
||||
mux.HandleFunc("POST /api/v1/players/teleport-to-player", handleTeleportToPlayer)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/events", handleGetPlayerEvents)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/dungeons", handleGetPlayerDungeons)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/stats", handleGetPlayerStats)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/solaris-history", handleGetSolarisHistory)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/session-history", handleGetSessionHistory)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/stat-snapshot-history", handleGetStatSnapshotHistory)
|
||||
|
||||
// ── database ──────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/database/tables", handleDBTables)
|
||||
mux.HandleFunc("GET /api/v1/database/describe", handleDBDescribe)
|
||||
mux.HandleFunc("GET /api/v1/database/sample", handleDBSample)
|
||||
mux.HandleFunc("GET /api/v1/database/search", handleDBSearch)
|
||||
mux.HandleFunc("POST /api/v1/database/sql", handleDBSQL)
|
||||
|
||||
// ── locations (editable teleport/spawn points) ───────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/locations", handleListLocations)
|
||||
mux.HandleFunc("POST /api/v1/locations", handleUpsertLocation)
|
||||
mux.HandleFunc("PUT /api/v1/locations", handleRenameLocation)
|
||||
mux.HandleFunc("DELETE /api/v1/locations", handleDeleteLocation)
|
||||
|
||||
// ── live map ────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/map/markers", handleGetMapMarkers)
|
||||
|
||||
// ── logs ──────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/logs/pods", handleLogPods)
|
||||
mux.HandleFunc("GET /api/v1/logs/stream", handleLogStream)
|
||||
mux.HandleFunc("GET /api/v1/logs/cheats", handleGetCheatLog)
|
||||
|
||||
// ── notifications ────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("POST /api/v1/notify", handleNotify)
|
||||
|
||||
// ── server commands (RabbitMQ, fire-and-forget) ───────────────────────────
|
||||
mux.HandleFunc("POST /api/v1/players/kick", handleRMQKickPlayer)
|
||||
mux.HandleFunc("POST /api/v1/players/fill-water", handleRMQFillWater)
|
||||
mux.HandleFunc("POST /api/v1/players/set-skill-points", handleRMQSetSkillPoints)
|
||||
mux.HandleFunc("POST /api/v1/players/clean-inventory", handleRMQCleanInventory)
|
||||
mux.HandleFunc("POST /api/v1/players/reset-progression", handleRMQResetProgression)
|
||||
mux.HandleFunc("POST /api/v1/players/set-skill-module", handleRMQSetSkillModule)
|
||||
mux.HandleFunc("POST /api/v1/players/give-item-live", handleRMQGiveItem)
|
||||
mux.HandleFunc("POST /api/v1/players/cheat-script", handleRMQCheatScript)
|
||||
mux.HandleFunc("POST /api/v1/vehicles/spawn", handleRMQSpawnVehicle)
|
||||
mux.HandleFunc("POST /api/v1/broadcast", handleRMQBroadcast)
|
||||
mux.HandleFunc("POST /api/v1/broadcast/shutdown", handleRMQBroadcastShutdown)
|
||||
mux.HandleFunc("POST /api/v1/chat/whisper", handleRMQWhisper)
|
||||
mux.HandleFunc("GET /api/v1/players/{id}/player-ids", handlePlayerIDDebug)
|
||||
|
||||
// ── storage ───────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/storage", handleListStorage)
|
||||
mux.HandleFunc("GET /api/v1/storage/{id}/items", handleGetStorageItems)
|
||||
mux.HandleFunc("POST /api/v1/storage/{id}/give-item", handleGiveItemToStorage)
|
||||
mux.HandleFunc("POST /api/v1/storage/{id}/give-items", handleGiveItemsToStorage)
|
||||
mux.HandleFunc("GET /api/v1/storage/{id}/owner-debug", handleStorageOwnerDebug)
|
||||
|
||||
// ── blueprints ────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/blueprints", handleListBlueprints)
|
||||
mux.HandleFunc("GET /api/v1/blueprints/{id}/export", handleExportBlueprint)
|
||||
mux.HandleFunc("POST /api/v1/blueprints/import", handleImportBlueprint)
|
||||
|
||||
// ── bases ─────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/bases", handleListBases)
|
||||
mux.HandleFunc("GET /api/v1/bases/{id}/export", handleExportBase)
|
||||
|
||||
// ── guilds ──────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/guilds", handleListGuilds)
|
||||
mux.HandleFunc("GET /api/v1/guilds/{id}", handleGetGuild)
|
||||
mux.HandleFunc("PATCH /api/v1/guilds/{id}", handleUpdateGuild)
|
||||
mux.HandleFunc("PUT /api/v1/guilds/{id}/members/{pid}/role", handleSetGuildMemberRole)
|
||||
|
||||
// ── landsraad (read-only) ─────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/landsraad", handleGetLandsraad)
|
||||
|
||||
// ── static data files (Go-first, CDN fallback on the frontend) ──────────
|
||||
mux.HandleFunc("GET /api/v1/data/{file}", handleGetDataFile)
|
||||
|
||||
// ── market board ─────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/market/items", handleMarketItems)
|
||||
mux.HandleFunc("GET /api/v1/market/listings", handleMarketListings)
|
||||
mux.HandleFunc("GET /api/v1/market/sales", handleMarketSales)
|
||||
mux.HandleFunc("GET /api/v1/market/stats", handleMarketStats)
|
||||
mux.HandleFunc("GET /api/v1/market/categories", handleMarketCategories)
|
||||
mux.HandleFunc("GET /api/v1/market/catalog", handleMarketCatalog)
|
||||
|
||||
// ── market bot control ────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/market-bot/status", handleMarketBotStatus)
|
||||
mux.HandleFunc("GET /api/v1/market-bot/config", handleMarketBotConfig)
|
||||
mux.HandleFunc("PUT /api/v1/market-bot/config", handleMarketBotConfig)
|
||||
mux.HandleFunc("POST /api/v1/market-bot/exec", handleMarketBotExec)
|
||||
mux.HandleFunc("POST /api/v1/market-bot/cleanup", handleMarketBotCleanup)
|
||||
mux.HandleFunc("GET /api/v1/market-bot/logs-ready", handleMarketBotLogsReady)
|
||||
mux.HandleFunc("GET /api/v1/market-bot/logs", handleMarketBotLogs)
|
||||
|
||||
// ── welcome package ───────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /api/v1/welcome-package/config", handleGetWelcomeConfig)
|
||||
mux.HandleFunc("PUT /api/v1/welcome-package/config", handlePutWelcomeConfig)
|
||||
mux.HandleFunc("GET /api/v1/welcome-package/grants", handleGetWelcomeGrants)
|
||||
mux.HandleFunc("POST /api/v1/welcome-package/retry", handleRetryWelcomeGrant)
|
||||
mux.HandleFunc("POST /api/v1/welcome-package/revoke", handleRevokeWelcomeGrant)
|
||||
mux.HandleFunc("POST /api/v1/welcome-package/run", handleRunWelcomePackage)
|
||||
|
||||
// ── give-items packs (operator-configurable pack library) ─────────────────
|
||||
mux.HandleFunc("GET /api/v1/give-packs/config", handleGetGivePacksConfig)
|
||||
mux.HandleFunc("PUT /api/v1/give-packs/config", handlePutGivePacksConfig)
|
||||
|
||||
// ── swagger UI ────────────────────────────────────────────────────────────
|
||||
mux.Handle("/swagger/", httpSwagger.WrapHandler)
|
||||
|
||||
// ── director reverse proxy (universal, opt-in) ──────────────────────────
|
||||
if loadedConfig.DirectorURL != "" {
|
||||
if target, err := url.Parse(loadedConfig.DirectorURL); err == nil {
|
||||
mux.HandleFunc("/director/", newDirectorProxy(target, dialThroughExecutor))
|
||||
log.Printf("Proxying /director/ → %s", loadedConfig.DirectorURL)
|
||||
}
|
||||
}
|
||||
|
||||
// SPA frontend: prefer the embedded FS (release builds with -tags=embed),
|
||||
// then fall back to a local dist directory for dev/AMP deployments.
|
||||
if fsys := embeddedSPAFS(); fsys != nil {
|
||||
log.Println("Serving frontend from embedded assets")
|
||||
mux.Handle("/", spaHandlerFS(fsys))
|
||||
} else {
|
||||
for _, dir := range []string{"./dist", "./web/dist"} {
|
||||
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
||||
log.Printf("Serving frontend from %s", dir)
|
||||
mux.Handle("/", spaHandler(dir))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: corsMiddleware(mux),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 10 * time.Minute, // backup/restore/download can take several minutes
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
log.Printf("dune-admin listening on %s", addr)
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
// spaHandler serves static files from distDir, falling back to index.html
|
||||
// for any path that does not match a real file (client-side routing).
|
||||
func spaHandler(distDir string) http.Handler {
|
||||
fileServer := http.FileServer(http.Dir(distDir))
|
||||
cleanDist := filepath.Clean(distDir)
|
||||
sep := string(filepath.Separator)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := filepath.Join(cleanDist, filepath.FromSlash(r.URL.Path))
|
||||
if p != cleanDist && !strings.HasPrefix(p, cleanDist+sep) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(p); err == nil { // #nosec G703 -- path validated against cleanDist prefix above
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath.Join(cleanDist, "index.html"))
|
||||
})
|
||||
}
|
||||
|
||||
// spaHandlerFS serves an embedded http.FileSystem as a SPA, falling back to
|
||||
// index.html for any path that does not map to a real file.
|
||||
//
|
||||
// Note: we open index.html directly instead of routing through http.FileServer
|
||||
// because FileServer always 301-redirects "/index.html" → "/" which creates an
|
||||
// infinite redirect loop (ERR_TOO_MANY_REDIRECTS) in browsers.
|
||||
func spaHandlerFS(fsys http.FileSystem) http.Handler {
|
||||
fileServer := http.FileServer(fsys)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && isRegularFile(fsys, r.URL.Path) {
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
f, err := fsys.Open("/index.html")
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, "index.html", fi.ModTime(), f)
|
||||
})
|
||||
}
|
||||
|
||||
func isRegularFile(fsys http.FileSystem, path string) bool {
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
fi, err := f.Stat()
|
||||
return err == nil && !fi.IsDir()
|
||||
}
|
||||
|
||||
// ── JSON helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
func jsonOK(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func jsonErr(w http.ResponseWriter, err error, code int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
func decode(r *http.Request, v any) error {
|
||||
return json.NewDecoder(r.Body).Decode(v)
|
||||
}
|
||||
|
||||
// handleStatus returns connection state and provider info.
|
||||
//
|
||||
// @Summary Return connection state and build info
|
||||
// @Tags status
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/status [get]
|
||||
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
executorType := "none"
|
||||
controlName := "none"
|
||||
if globalExecutor != nil {
|
||||
executorType = globalExecutor.Type()
|
||||
}
|
||||
if globalControl != nil {
|
||||
controlName = globalControl.Name()
|
||||
}
|
||||
jsonOK(w, map[string]any{
|
||||
"executor": executorType,
|
||||
"control": controlName,
|
||||
"ssh_connected": globalSSH != nil,
|
||||
"db_connected": globalDB != nil,
|
||||
"pod_ns": globalPodNS,
|
||||
"pod_ip": globalPodIP,
|
||||
"ssh_host": sshHost,
|
||||
"db_host": dbHost,
|
||||
"version": AppVersion,
|
||||
"commit": GitCommit,
|
||||
"build_time": BuildTime,
|
||||
"director_url": loadedConfig.DirectorURL,
|
||||
"listen_addr": loadedConfig.ListenAddr,
|
||||
})
|
||||
}
|
||||
|
||||
// handleReconnect tears down and re-establishes all connections.
|
||||
//
|
||||
// @Summary Tear down and re-establish all backend connections
|
||||
// @Tags status
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/reconnect [post]
|
||||
func handleReconnect(w http.ResponseWriter, r *http.Request) {
|
||||
if globalDB != nil {
|
||||
globalDB.Close()
|
||||
globalDB = nil
|
||||
}
|
||||
if globalExecutor != nil {
|
||||
globalExecutor.Close()
|
||||
globalExecutor = nil
|
||||
}
|
||||
globalSSH = nil
|
||||
globalControl = nil
|
||||
|
||||
if err := connectAll(); err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
handleStatus(w, r)
|
||||
}
|
||||
Reference in New Issue
Block a user