package handlers import ( "log" "net/http" "strconv" "strings" "time" "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) clientsList(w http.ResponseWriter, r *http.Request) { clients, err := db.ListClients(s.DB) if err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } s.render(w, "clients.html", pageData{ "Title": "Пользователи", "UserName": s.Auth.CurrentName(r), "Clients": clients, "Active": "clients", "Flash": r.URL.Query().Get("ok"), "Error": r.URL.Query().Get("err"), }) } func (s *Server) clientsNewGet(w http.ResponseWriter, r *http.Request) { inbounds, _ := db.ListAllInbounds(s.DB) s.render(w, "client_new.html", pageData{ "Title": "Новый пользователь", "UserName": s.Auth.CurrentName(r), "Active": "clients", "Inbounds": inbounds, "Form": map[string]any{ "Status": string(models.ClientStatusActive), }, }) } func parseInboundIDs(r *http.Request) []uuid.UUID { vals := r.Form["inbound_ids"] var ids []uuid.UUID for _, v := range vals { id, err := uuid.Parse(v) if err == nil { ids = append(ids, id) } } return ids } func parseExpireAt(raw string) *time.Time { raw = strings.TrimSpace(raw) if raw == "" { return nil } // datetime-local: 2006-01-02T15:04 for _, layout := range []string{"2006-01-02T15:04", "2006-01-02", time.RFC3339} { if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil { u := t.UTC() return &u } } return nil } func (s *Server) clientsCreate(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() username := strings.TrimSpace(r.FormValue("username")) email := strings.TrimSpace(r.FormValue("email")) note := strings.TrimSpace(r.FormValue("note")) status := models.ClientStatus(r.FormValue("status")) if status == "" { status = models.ClientStatusActive } gb, _ := strconv.ParseFloat(r.FormValue("traffic_limit_gb"), 64) var limitBytes int64 if gb > 0 { limitBytes = int64(gb * 1024 * 1024 * 1024) } expire := parseExpireAt(r.FormValue("expire_at")) inboundIDs := parseInboundIDs(r) inbounds, _ := db.ListAllInbounds(s.DB) formErr := func(msg string) { s.render(w, "client_new.html", pageData{ "Title": "Новый пользователь", "UserName": s.Auth.CurrentName(r), "Active": "clients", "Error": msg, "Inbounds": inbounds, "Form": map[string]any{ "Username": username, "Email": email, "Note": note, "Status": string(status), "TrafficLimitGB": r.FormValue("traffic_limit_gb"), "ExpireAt": r.FormValue("expire_at"), }, "SelectedInbounds": inboundIDs, }) } if username == "" { formErr("Укажите username") return } c := &models.Client{ Username: username, Email: email, Status: status, TrafficLimitBytes: limitBytes, ExpireAt: expire, Note: note, } if err := db.CreateClient(s.DB, c, inboundIDs); err != nil { log.Printf("create client: %v", err) formErr("Не удалось создать (возможно username занят)") return } s.syncNodesForInbounds(inboundIDs) http.Redirect(w, r, "/admin/clients/"+c.ID.String()+"?ok=created", http.StatusSeeOther) } func (s *Server) clientView(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 } c, err := db.GetClient(s.DB, id) if err != nil || c == nil { http.NotFound(w, r) return } inbounds, _ := db.ListAllInbounds(s.DB) info, _ := db.BuildSubscriptionInfo(s.DB, c) scheme := "https" if r.TLS == nil { if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" { scheme = xf } else { scheme = "http" } } subURL := scheme + "://" + r.Host + "/sub/" + c.SubToken var links []string var hosts []models.SubHost if info != nil { links = info.Links hosts = info.Hosts } s.render(w, "client_view.html", pageData{ "Title": c.Username, "UserName": s.Auth.CurrentName(r), "Active": "clients", "Client": c, "Inbounds": inbounds, "Links": links, "Hosts": hosts, "SubURL": subURL, "Flash": r.URL.Query().Get("ok"), "Error": r.URL.Query().Get("err"), }) } func (s *Server) clientUpdate(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() gb, _ := strconv.ParseFloat(r.FormValue("traffic_limit_gb"), 64) var limitBytes int64 if gb > 0 { limitBytes = int64(gb * 1024 * 1024 * 1024) } c := &models.Client{ ID: id, Username: strings.TrimSpace(r.FormValue("username")), Email: strings.TrimSpace(r.FormValue("email")), Status: models.ClientStatus(r.FormValue("status")), TrafficLimitBytes: limitBytes, ExpireAt: parseExpireAt(r.FormValue("expire_at")), Note: strings.TrimSpace(r.FormValue("note")), } if c.Username == "" { http.Redirect(w, r, "/admin/clients/"+id.String()+"?err=username", http.StatusSeeOther) return } oldIDs, _ := db.ListClientInboundIDs(s.DB, id) if err := db.UpdateClient(s.DB, c, parseInboundIDs(r)); err != nil { log.Printf("update client: %v", err) http.Redirect(w, r, "/admin/clients/"+id.String()+"?err=save", http.StatusSeeOther) return } newIDs := parseInboundIDs(r) merged := append([]uuid.UUID{}, oldIDs...) merged = append(merged, newIDs...) s.syncNodesForInbounds(merged) http.Redirect(w, r, "/admin/clients/"+id.String()+"?ok=saved", http.StatusSeeOther) } func (s *Server) clientToggle(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 } c, err := db.GetClient(s.DB, id) if err != nil || c == nil { http.NotFound(w, r) return } next := models.ClientStatusDisabled if c.Status != models.ClientStatusActive { next = models.ClientStatusActive } _ = db.SetClientStatus(s.DB, id, next) s.syncNodesForInbounds(c.InboundIDs) http.Redirect(w, r, "/admin/clients/"+id.String()+"?ok=status", http.StatusSeeOther) } func (s *Server) clientDelete(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 } c, _ := db.GetClient(s.DB, id) var inboundIDs []uuid.UUID if c != nil { inboundIDs = c.InboundIDs } _ = db.DeleteClient(s.DB, id) s.syncNodesForInbounds(inboundIDs) http.Redirect(w, r, "/admin/clients?ok=deleted", http.StatusSeeOther) }