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" "github.com/orohi/vpn-panel/internal/xraykeys" ) 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": inboundProtocols(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 parseInboundForm(r *http.Request, profileID, protocolID uuid.UUID, proto *models.Protocol) (*models.Inbound, error) { 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")) network := r.FormValue("network") security := r.FormValue("security") flow := strings.TrimSpace(r.FormValue("flow")) if security == "reality" && network == "tcp" && flow == "" && proto.Code == "vless" { flow = "xtls-rprx-vision" } in := &models.Inbound{ ProfileID: profileID, Tag: tag, ProtocolID: protocolID, ProtocolCode: proto.Code, Port: port, Network: network, Security: security, Listen: strings.TrimSpace(r.FormValue("listen")), Enabled: true, SortOrder: sortOrder, Remark: strings.TrimSpace(r.FormValue("remark")), Path: strings.TrimSpace(r.FormValue("path")), Host: strings.TrimSpace(r.FormValue("host")), SNI: strings.TrimSpace(r.FormValue("sni")), Fingerprint: strings.TrimSpace(r.FormValue("fingerprint")), Flow: flow, ALPN: strings.TrimSpace(r.FormValue("alpn")), RealityDest: strings.TrimSpace(r.FormValue("reality_dest")), RealityServerNames: strings.TrimSpace(r.FormValue("reality_server_names")), RealityPrivateKey: strings.TrimSpace(r.FormValue("reality_private_key")), RealityPublicKey: strings.TrimSpace(r.FormValue("reality_public_key")), RealityShortID: strings.TrimSpace(r.FormValue("reality_short_id")), SSMethod: strings.TrimSpace(r.FormValue("ss_method")), Password: strings.TrimSpace(r.FormValue("password")), } if in.Security == "reality" { priv, pub, sid, dest, names, err := xraykeys.EnsureReality( in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID, in.RealityDest, in.RealityServerNames, ) if err != nil { return nil, err } in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID = priv, pub, sid in.RealityDest, in.RealityServerNames = dest, names if in.SNI == "" { in.SNI = strings.Split(names, ",")[0] } if in.Fingerprint == "" { in.Fingerprint = "chrome" } } return in, nil } func (s *Server) syncProfileNodes(profileID uuid.UUID) { 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) } } } } func (s *Server) syncNodesForInbounds(inboundIDs []uuid.UUID) { nodes, err := db.ListOnlineNodesByInboundIDs(s.DB, inboundIDs) if err != nil { log.Printf("list nodes for inbounds: %v", err) return } for i := range nodes { _ = s.syncNodeConfig(&nodes[i]) } } 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 } switch proto.Code { case "vless", "vmess", "trojan", "shadowsocks": default: http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther) return } in, err := parseInboundForm(r, profileID, protocolID, proto) if err != nil { log.Printf("inbound form: %v", err) http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther) return } 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 } 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) inboundEditGet(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 } in, err := db.GetInbound(s.DB, inboundID) if err != nil || in == nil || in.ProfileID != profileID { http.NotFound(w, r) return } p, err := db.GetProfile(s.DB, profileID) if err != nil || p == nil { http.NotFound(w, r) return } protocols, _ := db.ListProtocols(s.DB) s.render(w, "inbound_edit.html", pageData{ "Title": "Inbound " + in.Tag, "UserName": s.Auth.CurrentName(r), "Active": "profiles", "Profile": p, "Inbound": in, "Protocols": inboundProtocols(protocols), "Flash": r.URL.Query().Get("ok"), "Error": r.URL.Query().Get("err"), }) } func inboundProtocols(all []models.Protocol) []models.Protocol { var out []models.Protocol for _, p := range all { switch p.Code { case "vless", "vmess", "trojan", "shadowsocks": out = append(out, p) } } return out } func (s *Server) inboundUpdate(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 } existing, err := db.GetInbound(s.DB, inboundID) if err != nil || existing == nil || existing.ProfileID != profileID { http.NotFound(w, r) return } _ = r.ParseForm() protocolID, err := uuid.Parse(r.FormValue("protocol_id")) if err != nil { http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.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()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther) return } switch proto.Code { case "vless", "vmess", "trojan", "shadowsocks": default: http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther) return } in, err := parseInboundForm(r, profileID, protocolID, proto) if err != nil { http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=keys", http.StatusSeeOther) return } in.ID = inboundID in.Enabled = false for _, v := range r.Form["enabled"] { if v == "on" || v == "true" { in.Enabled = true } } if in.Security == "reality" { if strings.TrimSpace(r.FormValue("reality_private_key")) == "" { in.RealityPrivateKey = existing.RealityPrivateKey } if strings.TrimSpace(r.FormValue("reality_public_key")) == "" { in.RealityPublicKey = existing.RealityPublicKey } if strings.TrimSpace(r.FormValue("reality_short_id")) == "" && existing.RealityShortID != "" { in.RealityShortID = existing.RealityShortID } } if err := db.UpdateInbound(s.DB, in); err != nil { log.Printf("update inbound: %v", err) http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=save", http.StatusSeeOther) return } s.syncProfileNodes(profileID) http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?ok=saved", 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) s.syncProfileNodes(profileID) 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) s.syncProfileNodes(profileID) 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) }