Add config profiles with inbound assignment for nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-29 04:41:41 +03:00
co-authored by Cursor
parent 1fb87f5f6c
commit 4991b313d7
16 changed files with 1122 additions and 25 deletions
+280
View File
@@ -0,0 +1,280 @@
package handlers
import (
"log"
"net/http"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/orohi/vpn-panel/internal/db"
"github.com/orohi/vpn-panel/internal/models"
)
func (s *Server) profilesList(w http.ResponseWriter, r *http.Request) {
profiles, err := db.ListProfiles(s.DB)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
s.render(w, "profiles.html", pageData{
"Title": "Профили",
"UserName": s.Auth.CurrentName(r),
"Profiles": profiles,
"Active": "profiles",
"Flash": r.URL.Query().Get("ok"),
"Error": r.URL.Query().Get("err"),
})
}
func (s *Server) profilesNewGet(w http.ResponseWriter, r *http.Request) {
s.render(w, "profile_new.html", pageData{
"Title": "Новый профиль",
"UserName": s.Auth.CurrentName(r),
"Active": "profiles",
})
}
func (s *Server) profilesCreate(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
name := strings.TrimSpace(r.FormValue("name"))
desc := strings.TrimSpace(r.FormValue("description"))
if name == "" {
s.render(w, "profile_new.html", pageData{
"Title": "Новый профиль", "UserName": s.Auth.CurrentName(r), "Active": "profiles",
"Error": "Укажите имя профиля", "Form": map[string]any{"Name": name, "Description": desc},
})
return
}
p := &models.ConfigProfile{Name: name, Description: desc}
if err := db.CreateProfile(s.DB, p); err != nil {
log.Printf("create profile: %v", err)
s.render(w, "profile_new.html", pageData{
"Title": "Новый профиль", "UserName": s.Auth.CurrentName(r), "Active": "profiles",
"Error": "Не удалось создать (возможно имя занято)", "Form": map[string]any{"Name": name, "Description": desc},
})
return
}
http.Redirect(w, r, "/admin/profiles/"+p.ID.String()+"?ok=created", http.StatusSeeOther)
}
func (s *Server) profileView(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
p, err := db.GetProfile(s.DB, id)
if err != nil || p == nil {
http.NotFound(w, r)
return
}
protocols, _ := db.ListProtocols(s.DB)
s.render(w, "profile_view.html", pageData{
"Title": p.Name,
"UserName": s.Auth.CurrentName(r),
"Active": "profiles",
"Profile": p,
"Protocols": protocols,
"Flash": r.URL.Query().Get("ok"),
"Error": r.URL.Query().Get("err"),
})
}
func (s *Server) profileUpdate(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
_ = r.ParseForm()
p := &models.ConfigProfile{
ID: id,
Name: strings.TrimSpace(r.FormValue("name")),
Description: strings.TrimSpace(r.FormValue("description")),
}
if p.Name == "" {
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?err=name", http.StatusSeeOther)
return
}
if err := db.UpdateProfile(s.DB, p); err != nil {
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?err=save", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?ok=saved", http.StatusSeeOther)
}
func (s *Server) profileDelete(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
if err := db.DeleteProfile(s.DB, id); err != nil {
http.Redirect(w, r, "/admin/profiles?err=delete", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/profiles?ok=deleted", http.StatusSeeOther)
}
func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
profileID, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
_ = r.ParseForm()
protocolID, err := uuid.Parse(r.FormValue("protocol_id"))
if err != nil {
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
return
}
proto, err := db.GetProtocol(s.DB, protocolID)
if err != nil || proto == nil {
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
return
}
port, _ := strconv.Atoi(r.FormValue("port"))
if port <= 0 {
port = proto.Port
if port <= 0 {
port = 443
}
}
tag := strings.TrimSpace(r.FormValue("tag"))
if tag == "" {
tag = strings.ToUpper(proto.Code) + "-" + strconv.Itoa(port)
}
sortOrder, _ := strconv.Atoi(r.FormValue("sort_order"))
in := &models.Inbound{
ProfileID: profileID,
Tag: tag,
ProtocolID: protocolID,
ProtocolCode: proto.Code,
Port: port,
Network: r.FormValue("network"),
Security: r.FormValue("security"),
Listen: strings.TrimSpace(r.FormValue("listen")),
Enabled: true,
SortOrder: sortOrder,
Remark: strings.TrimSpace(r.FormValue("remark")),
}
if err := db.CreateInbound(s.DB, in); err != nil {
log.Printf("create inbound: %v", err)
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther)
return
}
// Enable on all nodes already using this profile.
nodes, _ := db.ListNodes(s.DB)
for _, n := range nodes {
if n.ProfileID != nil && *n.ProfileID == profileID {
_ = db.SetNodeInboundEnabled(s.DB, n.ID, in.ID, true)
if n.Status == models.NodeStatusOnline {
nn := n
_ = s.syncNodeConfig(&nn)
}
}
}
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_added", http.StatusSeeOther)
}
func (s *Server) inboundDelete(w http.ResponseWriter, r *http.Request) {
profileID, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
if err != nil {
http.Error(w, "invalid inbound", http.StatusBadRequest)
return
}
_ = db.DeleteInbound(s.DB, inboundID)
nodes, _ := db.ListNodes(s.DB)
for _, n := range nodes {
if n.ProfileID != nil && *n.ProfileID == profileID {
_ = db.ApplyProfileInboundsToNodeProtocols(s.DB, n.ID)
if n.Status == models.NodeStatusOnline {
nn := n
_ = s.syncNodeConfig(&nn)
}
}
}
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_deleted", http.StatusSeeOther)
}
func (s *Server) inboundToggle(w http.ResponseWriter, r *http.Request) {
profileID, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
if err != nil {
http.Error(w, "invalid inbound", http.StatusBadRequest)
return
}
_ = db.ToggleInbound(s.DB, inboundID)
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_toggled", http.StatusSeeOther)
}
func (s *Server) nodeAssignProfile(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
}
_ = r.ParseForm()
raw := strings.TrimSpace(r.FormValue("profile_id"))
n, err := db.GetNode(s.DB, nodeID)
if err != nil || n == nil {
http.NotFound(w, r)
return
}
if raw == "" || raw == "none" {
_ = db.ClearNodeProfile(s.DB, nodeID)
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=profile_cleared", http.StatusSeeOther)
return
}
profileID, err := uuid.Parse(raw)
if err != nil {
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=profile", http.StatusSeeOther)
return
}
if err := db.AssignProfileToNode(s.DB, nodeID, profileID); err != nil {
log.Printf("assign profile: %v", err)
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=profile_assign", http.StatusSeeOther)
return
}
n2, _ := db.GetNode(s.DB, nodeID)
if n2 != nil && n2.Status == models.NodeStatusOnline {
_ = s.syncNodeConfig(n2)
}
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=profile_assigned", http.StatusSeeOther)
}
func (s *Server) nodeInboundToggle(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
}
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
if err != nil {
http.Error(w, "invalid inbound", http.StatusBadRequest)
return
}
_ = r.ParseForm()
enabled := r.FormValue("action") == "enable"
if err := db.SetNodeInboundEnabled(s.DB, nodeID, inboundID, enabled); err != nil {
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=inbound", http.StatusSeeOther)
return
}
n, _ := db.GetNode(s.DB, nodeID)
if n != nil && n.Status == models.NodeStatusOnline {
_ = s.syncNodeConfig(n)
}
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=inbound_updated", http.StatusSeeOther)
}