package web import ( "fmt" "net/http" "strconv" "github.com/owen/vibeproxy/internal/auth" "github.com/owen/vibeproxy/internal/database" "github.com/owen/vibeproxy/internal/logging" "github.com/owen/vibeproxy/internal/models" ) func (h *Web) handleStreamsList(w http.ResponseWriter, r *http.Request) { streams, err := database.ListStreams(h.db) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } render(w, "streams", PageData{ Active: "streams", Title: "TCP/UDP Streams", CSRFToken: h.csrf.Token(w), Flash: r.URL.Query().Get("flash"), FlashKind: r.URL.Query().Get("kind"), Data: streams, }) } func (h *Web) handleStreamForm(w http.ResponseWriter, r *http.Request) { var s models.Stream isNew := true if idStr := r.PathValue("id"); idStr != "" { isNew = false id, _ := strconv.ParseInt(idStr, 10, 64) existing, err := database.GetStream(h.db, id) if err != nil { http.Error(w, "stream not found", http.StatusNotFound) return } s = *existing } else { s = models.Stream{Protocol: models.StreamTCP, ConnectTimeoutMS: 5000, IdleTimeoutMS: 300000, Enabled: true} } render(w, "stream_form", PageData{ Active: "streams", Title: pageTitle(isNew, "Stream"), CSRFToken: h.csrf.Token(w), Data: map[string]any{"Stream": s, "IsNew": isNew}, }) } func (h *Web) handleStreamCreate(w http.ResponseWriter, r *http.Request) { h.saveStreamForm(w, r, 0) } func (h *Web) handleStreamUpdate(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) h.saveStreamForm(w, r, id) } func (h *Web) saveStreamForm(w http.ResponseWriter, r *http.Request, id int64) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } s := models.Stream{ ID: id, Protocol: models.StreamProtocol(r.FormValue("protocol")), ListenIP: r.FormValue("listen_ip"), ListenPort: atoiDefault(r.FormValue("listen_port"), 0), DestinationHost: r.FormValue("destination_host"), DestinationPort: atoiDefault(r.FormValue("destination_port"), 0), ConnectTimeoutMS: atoiDefault(r.FormValue("connect_timeout_ms"), 5000), IdleTimeoutMS: atoiDefault(r.FormValue("idle_timeout_ms"), 300000), ProxyProtocol: r.FormValue("proxy_protocol") == "on", Enabled: r.FormValue("enabled") == "on", Comments: r.FormValue("comments"), } if s.ListenPort <= 0 || s.ListenPort > 65535 || s.DestinationHost == "" || s.DestinationPort <= 0 { render(w, "stream_form", PageData{ Active: "streams", Title: pageTitle(id == 0, "Stream"), CSRFToken: h.csrf.Token(w), Flash: "Please provide a valid listen port and destination.", FlashKind: "danger", Data: map[string]any{"Stream": s, "IsNew": id == 0}, }) return } if _, err := database.SaveStream(h.db, &s); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h.reloadStreams(r) logging.Audit(h.db, auth.ActorFromContext(r.Context()), "stream_changed", fmt.Sprintf("%s:%d", s.Protocol, s.ListenPort), "") http.Redirect(w, r, "/streams?flash="+flashMsg("Stream saved")+"&kind=success", http.StatusFound) } func (h *Web) handleStreamDelete(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) if err := database.DeleteStream(h.db, id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h.reloadStreams(r) logging.Audit(h.db, auth.ActorFromContext(r.Context()), "stream_changed", fmt.Sprintf("id=%d", id), "deleted") http.Redirect(w, r, "/streams?flash="+flashMsg("Stream deleted")+"&kind=success", http.StatusFound) } func (h *Web) handleStreamToggle(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) existing, err := database.GetStream(h.db, id) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } if err := database.SetStreamEnabled(h.db, id, !existing.Enabled); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h.reloadStreams(r) logging.Audit(h.db, auth.ActorFromContext(r.Context()), "stream_changed", fmt.Sprintf("id=%d", id), fmt.Sprintf("enabled=%v", !existing.Enabled)) http.Redirect(w, r, "/streams", http.StatusFound) }