Files
pawsql/internal/adminui/handler.go
Shaun Campbell 7d7133d6cc
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 6m32s
Test and Release PawSQL / test (push) Successful in 50s
Test and Release PawSQL / release (push) Successful in 8s
feat: ship Barkstack UI plugin
2026-09-16 13:47:07 -04:00

119 lines
3.5 KiB
Go

// Package adminui serves PawSQL's Barkstack UI plugin and small read-only status API.
package adminui
import (
"embed"
"encoding/json"
"io/fs"
"mime"
"net/http"
"path"
"strings"
)
//go:embed dist
var embeddedUI embed.FS
const entryPath = "/barkstack/ui/assets/entry.js"
type manifest struct {
APIVersion string `json:"apiVersion"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Mount string `json:"mount"`
Entry string `json:"entry"`
Element string `json:"element"`
Navigation []navigationItem `json:"navigation"`
}
type navigationItem struct {
Label string `json:"label"`
Path string `json:"path"`
Icon string `json:"icon"`
}
type RouteSummary struct {
Name string `json:"name"`
Hostname string `json:"hostname,omitempty"`
Managed bool `json:"managed"`
}
type statusResponse struct {
Status string `json:"status"`
Routes []RouteSummary `json:"routes"`
Endpoint string `json:"endpoint"`
}
type handler struct {
assets fs.FS
routes []RouteSummary
}
func Handler(routes []RouteSummary) http.Handler {
assets, err := fs.Sub(embeddedUI, "dist")
if err != nil {
panic(err)
}
copyRoutes := append([]RouteSummary(nil), routes...)
return &handler{assets: assets, routes: copyRoutes}
}
func (h *handler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet && request.Method != http.MethodHead {
response.Header().Set("Allow", "GET, HEAD")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
switch request.URL.Path {
case "/barkstack/ui/manifest.json":
h.serveJSON(response, request, manifest{
APIVersion: "barkstack.dev/ui/v1", ID: "pawsql", Name: "PawSQL",
Description: "PostgreSQL routing for Barkstack", Icon: "database", Mount: "/pawsql",
Entry: entryPath, Element: "barkstack-pawsql",
Navigation: []navigationItem{{Label: "PawSQL", Path: "/pawsql", Icon: "database"}},
})
case "/barkstack/ui/status.json":
h.serveJSON(response, request, statusResponse{Status: "running", Routes: h.routes, Endpoint: "*.pawsql.barkstack.dev:5432"})
default:
h.serveAsset(response, request)
}
}
func (h *handler) serveJSON(response http.ResponseWriter, request *http.Request, value any) {
response.Header().Set("Content-Type", "application/json")
response.Header().Set("Cache-Control", "no-store")
if request.Method == http.MethodHead {
return
}
_ = json.NewEncoder(response).Encode(value)
}
func (h *handler) serveAsset(response http.ResponseWriter, request *http.Request) {
const prefix = "/barkstack/ui/assets/"
if !strings.HasPrefix(request.URL.Path, prefix) {
http.NotFound(response, request)
return
}
assetName := strings.TrimPrefix(request.URL.Path, "/barkstack/ui/")
if assetName == "" || !fs.ValidPath(assetName) || path.Clean(assetName) != assetName || strings.Contains(assetName, "\\") {
http.NotFound(response, request)
return
}
contents, err := fs.ReadFile(h.assets, assetName)
if err != nil {
http.NotFound(response, request)
return
}
if contentType := mime.TypeByExtension(path.Ext(assetName)); contentType != "" {
response.Header().Set("Content-Type", contentType)
}
response.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
response.Header().Set("X-Content-Type-Options", "nosniff")
if request.Method == http.MethodHead {
return
}
_, _ = response.Write(contents)
}