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,121 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/owen/vibeproxy/internal/auth"
|
||||
"github.com/owen/vibeproxy/internal/certificates"
|
||||
"github.com/owen/vibeproxy/internal/database"
|
||||
"github.com/owen/vibeproxy/internal/logging"
|
||||
"github.com/owen/vibeproxy/internal/models"
|
||||
)
|
||||
|
||||
func (h *Web) handleCertificatesList(w http.ResponseWriter, r *http.Request) {
|
||||
certs, err := database.ListCertificates(h.db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render(w, "certificates", PageData{
|
||||
Active: "certificates", Title: "Certificates", CSRFToken: h.csrf.Token(w),
|
||||
Flash: r.URL.Query().Get("flash"), FlashKind: r.URL.Query().Get("kind"),
|
||||
Data: certs,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Web) handleCertificateIssue(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := r.FormValue("name")
|
||||
domains := splitLines(r.FormValue("domains"))
|
||||
if name == "" || len(domains) == 0 {
|
||||
h.redirectCertError(w, r, "Name and at least one domain are required")
|
||||
return
|
||||
}
|
||||
settings := h.store.Current().Settings
|
||||
email := r.FormValue("email")
|
||||
if email == "" {
|
||||
email = settings.ACMEEmail
|
||||
}
|
||||
challengeType := models.ChallengeType(r.FormValue("challenge_type"))
|
||||
if challengeType == "" {
|
||||
challengeType = models.ChallengeHTTP01
|
||||
}
|
||||
cert, err := h.certs.IssueLetsEncrypt(r.Context(), certificates.IssueLetsEncryptOptions{
|
||||
Name: name, Domains: domains, Email: email, Staging: settings.ACMEStaging,
|
||||
ChallengeType: challengeType, DNSProvider: r.FormValue("dns_provider"),
|
||||
})
|
||||
if err != nil {
|
||||
h.redirectCertError(w, r, "Issuance failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "certificate_issued", cert.Name, fmt.Sprintf("domains=%v", cert.Domains))
|
||||
http.Redirect(w, r, "/certificates?flash="+flashMsg("Certificate issued")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Web) handleCertificateUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
name := r.FormValue("name")
|
||||
domains := splitLines(r.FormValue("domains"))
|
||||
certPEM := r.FormValue("cert_pem")
|
||||
keyPEM := r.FormValue("key_pem")
|
||||
if name == "" || certPEM == "" || keyPEM == "" {
|
||||
h.redirectCertError(w, r, "Name, certificate, and private key are required")
|
||||
return
|
||||
}
|
||||
cert, err := h.certs.UploadCustom(name, domains, []byte(certPEM), []byte(keyPEM))
|
||||
if err != nil {
|
||||
h.redirectCertError(w, r, "Upload failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "certificate_issued", cert.Name, "custom certificate uploaded")
|
||||
http.Redirect(w, r, "/certificates?flash="+flashMsg("Certificate uploaded")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Web) redirectCertError(w http.ResponseWriter, r *http.Request, msg string) {
|
||||
http.Redirect(w, r, "/certificates?flash="+flashMsg(msg)+"&kind=danger", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Web) handleCertificateRenew(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
c, err := database.GetCertificate(h.db, id)
|
||||
if err != nil {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if c.Type != models.CertTypeLetsEncrypt {
|
||||
h.redirectCertError(w, r, "Only Let's Encrypt certificates can be renewed here")
|
||||
return
|
||||
}
|
||||
settings := h.store.Current().Settings
|
||||
_, err = h.certs.IssueLetsEncrypt(r.Context(), certificates.IssueLetsEncryptOptions{
|
||||
Name: c.Name, Domains: c.Domains, Email: settings.ACMEEmail, Staging: settings.ACMEStaging,
|
||||
ChallengeType: c.ChallengeType, DNSProvider: c.DNSProvider,
|
||||
})
|
||||
if err != nil {
|
||||
h.redirectCertError(w, r, "Renewal failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "certificate_renewed", c.Name, "")
|
||||
http.Redirect(w, r, "/certificates?flash="+flashMsg("Certificate renewed")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Web) handleCertificateDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err := database.DeleteCertificate(h.db, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.certs.LoadAll()
|
||||
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "certificate_changed", fmt.Sprintf("id=%d", id), "deleted")
|
||||
http.Redirect(w, r, "/certificates?flash="+flashMsg("Certificate deleted")+"&kind=success", http.StatusFound)
|
||||
}
|
||||
Reference in New Issue
Block a user