239 lines
6.6 KiB
Go
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)
|
|
}
|