Show protocol enable state and which nodes have each protocol installed.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/orohi/vpn-panel/internal/auth"
|
||||
@@ -72,7 +71,9 @@ func (s *Server) Routes() http.Handler {
|
||||
r.HandleFunc("/admin/nodes/{id}", s.Auth.RequireAuth(s.nodeView)).Methods(http.MethodGet)
|
||||
r.HandleFunc("/admin/nodes/{id}/install", s.Auth.RequireAuth(s.nodeInstall)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/nodes/{id}/check", s.Auth.RequireAuth(s.nodeCheck)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/nodes/{id}/sync", s.Auth.RequireAuth(s.nodeSyncProtocols)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/nodes/{id}/delete", s.Auth.RequireAuth(s.nodeDelete)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/nodes/{id}/protocols/{protocolID}", s.Auth.RequireAuth(s.nodeProtocolAction)).Methods(http.MethodPost)
|
||||
|
||||
r.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -150,7 +151,7 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
protocols, err := db.ListProtocols(s.DB)
|
||||
protocols, err := db.ListProtocolsDetailed(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -165,31 +166,3 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
"Active": "dashboard",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) protocols(w http.ResponseWriter, r *http.Request) {
|
||||
protocols, err := db.ListProtocols(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.render(w, "protocols.html", pageData{
|
||||
"Title": "Протоколы",
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Protocols": protocols,
|
||||
"Active": "protocols",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) toggleProtocol(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := db.ToggleProtocol(s.DB, id); err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/protocols", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -148,15 +148,21 @@ func (s *Server) nodeView(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
nodeProtocols, err := db.ListNodeProtocols(s.DB, id)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.render(w, "node_view.html", pageData{
|
||||
"Title": n.Name,
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Active": "nodes",
|
||||
"Node": n,
|
||||
"Compose": nodeinstall.ComposeYAML(n),
|
||||
"ManualScript": nodeinstall.ManualInstallScript(n),
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
"Title": n.Name,
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Active": "nodes",
|
||||
"Node": n,
|
||||
"NodeProtocols": nodeProtocols,
|
||||
"Compose": nodeinstall.ComposeYAML(n),
|
||||
"ManualScript": nodeinstall.ManualInstallScript(n),
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,6 +203,10 @@ func (s *Server) nodeCheck(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
_ = db.MarkNodeSeen(s.DB, id, hr.Version, models.NodeStatusOnline)
|
||||
// Panel is source of truth — push desired protocols to the agent.
|
||||
if err := s.syncNodeConfig(n); err != nil {
|
||||
log.Printf("push config to node %s: %v", id, err)
|
||||
}
|
||||
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?ok=online", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/orohi/vpn-panel/internal/db"
|
||||
"github.com/orohi/vpn-panel/internal/models"
|
||||
"github.com/orohi/vpn-panel/internal/nodeclient"
|
||||
)
|
||||
|
||||
func (s *Server) syncNodeConfig(n *models.Node) error {
|
||||
cfg, err := db.NodeProtocolConfigPayload(s.DB, n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nodeclient.PushConfig(n, cfg)
|
||||
}
|
||||
|
||||
func (s *Server) protocols(w http.ResponseWriter, r *http.Request) {
|
||||
protocols, err := db.ListProtocolsDetailed(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
nodes, _ := db.ListNodes(s.DB)
|
||||
s.render(w, "protocols.html", pageData{
|
||||
"Title": "Протоколы",
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Protocols": protocols,
|
||||
"Nodes": nodes,
|
||||
"Active": "protocols",
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) toggleProtocol(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := db.ToggleProtocol(s.DB, id); err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/protocols", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) nodeProtocolAction(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid node id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
protocolID, err := uuid.Parse(mux.Vars(r)["protocolID"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid protocol id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
action := r.FormValue("action")
|
||||
if action == "" {
|
||||
_ = r.ParseForm()
|
||||
action = r.FormValue("action")
|
||||
}
|
||||
|
||||
n, err := db.GetNode(s.DB, nodeID)
|
||||
if err != nil || n == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
p, err := db.GetProtocol(s.DB, protocolID)
|
||||
if err != nil || p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := db.ListNodeProtocols(s.DB, nodeID)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var current models.NodeProtocol
|
||||
for _, item := range list {
|
||||
if item.ProtocolID == protocolID {
|
||||
current = item
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
installed, enabled := current.Installed, current.Enabled
|
||||
switch action {
|
||||
case "install":
|
||||
installed, enabled = true, true
|
||||
case "uninstall":
|
||||
installed, enabled = false, false
|
||||
case "enable":
|
||||
installed, enabled = true, true
|
||||
case "disable":
|
||||
enabled = false
|
||||
if !installed {
|
||||
installed = false
|
||||
}
|
||||
default:
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=bad_action", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.SetNodeProtocolFlags(s.DB, nodeID, protocolID, installed, enabled); err != nil {
|
||||
log.Printf("set node protocol: %v", err)
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=proto_save", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// If panel catalog was off and we install/enable — turn protocol on in catalog.
|
||||
if (action == "install" || action == "enable") && !p.Enabled {
|
||||
_ = db.SetProtocolEnabled(s.DB, protocolID, true)
|
||||
}
|
||||
if action == "uninstall" {
|
||||
// If nowhere else has it installed, turn catalog off.
|
||||
detailed, _ := db.ListProtocolsDetailed(s.DB)
|
||||
for _, item := range detailed {
|
||||
if item.ID == protocolID && len(item.InstalledOn) == 0 {
|
||||
_ = db.SetProtocolEnabled(s.DB, protocolID, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n.Status == models.NodeStatusOnline {
|
||||
if err := s.syncNodeConfig(n); err != nil {
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=sync_failed", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=protocol_"+action, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) nodeSyncProtocols(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
n, err := db.GetNode(s.DB, nodeID)
|
||||
if err != nil || n == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
hr, err := nodeclient.Health(n)
|
||||
if err != nil {
|
||||
_ = db.UpdateNodeStatus(s.DB, nodeID, models.NodeStatusOffline, err.Error())
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=offline", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
_ = db.MarkNodeSeen(s.DB, nodeID, hr.Version, models.NodeStatusOnline)
|
||||
// Push panel → node (desired state). Do not wipe local DB from empty agent.
|
||||
if err := s.syncNodeConfig(n); err != nil {
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=sync_failed", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=synced", http.StatusSeeOther)
|
||||
}
|
||||
Reference in New Issue
Block a user