Files
panel-vpn/internal/handlers/clients.go
T

239 lines
6.6 KiB
Go

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)
}