Initial vibeproxy implementation
Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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) handleAccessListsList(w http.ResponseWriter, r *http.Request) {
|
||||
lists, err := database.ListAccessLists(h.db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render(w, "access_lists", PageData{
|
||||
Active: "access_lists", Title: "Access Lists", CSRFToken: h.csrf.Token(w),
|
||||
Flash: r.URL.Query().Get("flash"), FlashKind: r.URL.Query().Get("kind"),
|
||||
Data: lists,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Web) handleAccessListForm(w http.ResponseWriter, r *http.Request) {
|
||||
var l models.AccessList
|
||||
isNew := true
|
||||
if idStr := r.PathValue("id"); idStr != "" {
|
||||
isNew = false
|
||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||
existing, err := database.GetAccessList(h.db, id)
|
||||
if err != nil {
|
||||
http.Error(w, "access list not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
l = *existing
|
||||
}
|
||||
render(w, "access_list_form", PageData{
|
||||
Active: "access_lists", Title: pageTitle(isNew, "Access List"), CSRFToken: h.csrf.Token(w),
|
||||
Data: map[string]any{"List": l, "IsNew": isNew},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Web) handleAccessListCreate(w http.ResponseWriter, r *http.Request) {
|
||||
h.saveAccessListForm(w, r, 0)
|
||||
}
|
||||
|
||||
func (h *Web) handleAccessListUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
h.saveAccessListForm(w, r, id)
|
||||
}
|
||||
|
||||
func (h *Web) saveAccessListForm(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
l := models.AccessList{
|
||||
ID: id,
|
||||
Name: r.FormValue("name"),
|
||||
DefaultAllow: r.FormValue("default_allow") == "on",
|
||||
Comments: r.FormValue("comments"),
|
||||
}
|
||||
actions := r.Form["rule_action"]
|
||||
cidrs := r.Form["rule_cidr"]
|
||||
for i := range cidrs {
|
||||
if cidrs[i] == "" {
|
||||
continue
|
||||
}
|
||||
l.Rules = append(l.Rules, models.AccessRule{
|
||||
Action: models.AccessRuleAction(get(actions, i)),
|
||||
CIDR: cidrs[i],
|
||||
Priority: i,
|
||||
})
|
||||
}
|
||||
existingHashes := map[string]string{}
|
||||
if id != 0 {
|
||||
if existing, err := database.GetAccessList(h.db, id); err == nil {
|
||||
for _, u := range existing.BasicAuthUsers {
|
||||
existingHashes[u.Username] = u.PasswordHash
|
||||
}
|
||||
}
|
||||
}
|
||||
usernames := r.Form["auth_username"]
|
||||
passwords := r.Form["auth_password"]
|
||||
for i := range usernames {
|
||||
if usernames[i] == "" {
|
||||
continue
|
||||
}
|
||||
hash := ""
|
||||
if passwords[i] != "" {
|
||||
h2, err := auth.HashPassword(passwords[i])
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
hash = h2
|
||||
} else if existing, ok := existingHashes[usernames[i]]; ok {
|
||||
hash = existing // password left blank on edit: keep the existing hash
|
||||
} else {
|
||||
continue // new user with no password set — skip
|
||||
}
|
||||
l.BasicAuthUsers = append(l.BasicAuthUsers, models.AccessListUser{Username: usernames[i], PasswordHash: hash})
|
||||
}
|
||||
if l.Name == "" {
|
||||
render(w, "access_list_form", PageData{
|
||||
Active: "access_lists", Title: pageTitle(id == 0, "Access List"), CSRFToken: h.csrf.Token(w),
|
||||
Flash: "Name is required", FlashKind: "danger",
|
||||
Data: map[string]any{"List": l, "IsNew": id == 0},
|
||||
})
|
||||
return
|
||||
}
|
||||
if _, err := database.SaveAccessList(h.db, &l); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.reload(r)
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "access_list_changed", l.Name, "")
|
||||
http.Redirect(w, r, "/access-lists?flash="+flashMsg("Access list saved")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Web) handleAccessListDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err := database.DeleteAccessList(h.db, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.reload(r)
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "access_list_changed", fmt.Sprintf("id=%d", id), "deleted")
|
||||
http.Redirect(w, r, "/access-lists?flash="+flashMsg("Access list deleted")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
Reference in New Issue
Block a user