// Package webui serves a small read-only web UI over the discovered cloud // assets. It is intentionally dependency-free (stdlib + embedded HTML) and // exposes no mutation endpoints. package webui import ( "context" _ "embed" "encoding/json" "net/http" "strconv" "strings" "tea.karlcom.de/karl/bucketeer/internal/store" ) //go:embed index.html var indexHTML []byte // Store is the read-only subset of the database the UI needs. type Store interface { ListAssets(ctx context.Context, f store.AssetFilter) ([]store.Asset, error) Stats(ctx context.Context) (store.Stats, error) } type Config struct { User string // optional HTTP basic auth username Pass string // optional HTTP basic auth password } // New returns the UI handler. If Config.User is set, all routes require basic auth. func New(s Store, cfg Config) http.Handler { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(indexHTML) }) mux.HandleFunc("/api/assets", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() limit, _ := strconv.Atoi(q.Get("limit")) offset, _ := strconv.Atoi(q.Get("offset")) assets, err := s.ListAssets(r.Context(), store.AssetFilter{ Provider: strings.TrimSpace(q.Get("provider")), Service: strings.TrimSpace(q.Get("service")), Exposure: strings.TrimSpace(q.Get("exposure")), Query: strings.TrimSpace(q.Get("q")), Limit: limit, Offset: offset, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, assets) }) mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) { st, err := s.Stats(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, st) }) if cfg.User != "" { return basicAuth(cfg.User, cfg.Pass, mux) } return mux } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(v) } func basicAuth(user, pass string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok || u != user || p != pass { w.Header().Set("WWW-Authenticate", `Basic realm="ctcloud"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) }