Show protocol enable state and which nodes have each protocol installed.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+36
-14
@@ -17,6 +17,7 @@ type Agent struct {
|
|||||||
name string
|
name string
|
||||||
secret string
|
secret string
|
||||||
version string
|
version string
|
||||||
|
dataDir string
|
||||||
startedAt time.Time
|
startedAt time.Time
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
config map[string]any
|
config map[string]any
|
||||||
@@ -30,7 +31,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
name := env("NODE_NAME", "vpn-node")
|
name := env("NODE_NAME", "vpn-node")
|
||||||
version := env("AGENT_VERSION", defaultVersion)
|
version := env("AGENT_VERSION", defaultVersion)
|
||||||
|
|
||||||
dataDir := env("DATA_DIR", "/var/lib/vpn-node")
|
dataDir := env("DATA_DIR", "/var/lib/vpn-node")
|
||||||
_ = os.MkdirAll(dataDir, 0o755)
|
_ = os.MkdirAll(dataDir, 0o755)
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ func main() {
|
|||||||
name: name,
|
name: name,
|
||||||
secret: secret,
|
secret: secret,
|
||||||
version: version,
|
version: version,
|
||||||
|
dataDir: dataDir,
|
||||||
startedAt: time.Now().UTC(),
|
startedAt: time.Now().UTC(),
|
||||||
config: map[string]any{},
|
config: map[string]any{},
|
||||||
}
|
}
|
||||||
@@ -77,23 +78,44 @@ func (a *Agent) auth(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stringList(cfg map[string]any, key string) []string {
|
||||||
|
out := []string{}
|
||||||
|
raw, ok := cfg[key]
|
||||||
|
if !ok {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case []any:
|
||||||
|
for _, item := range v {
|
||||||
|
if s, ok := item.(string); ok && s != "" {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
out = append(out, v...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) health(w http.ResponseWriter, _ *http.Request) {
|
func (a *Agent) health(w http.ResponseWriter, _ *http.Request) {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
protocols := []string{}
|
enabled := stringList(a.config, "protocols_enabled")
|
||||||
if raw, ok := a.config["protocols"].([]any); ok {
|
if len(enabled) == 0 {
|
||||||
for _, p := range raw {
|
enabled = stringList(a.config, "protocols")
|
||||||
if s, ok := p.(string); ok {
|
}
|
||||||
protocols = append(protocols, s)
|
installed := stringList(a.config, "protocols_installed")
|
||||||
}
|
if len(installed) == 0 {
|
||||||
}
|
installed = append([]string{}, enabled...)
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{
|
writeJSON(w, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"name": a.name,
|
"name": a.name,
|
||||||
"version": a.version,
|
"version": a.version,
|
||||||
"uptime_sec": int64(time.Since(a.startedAt).Seconds()),
|
"uptime_sec": int64(time.Since(a.startedAt).Seconds()),
|
||||||
"protocols": protocols,
|
"protocols": enabled,
|
||||||
|
"protocols_installed": installed,
|
||||||
|
"protocols_enabled": enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +145,7 @@ func (a *Agent) configHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
a.config = cfg
|
a.config = cfg
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
_ = a.saveConfig("/var/lib/vpn-node/config.json")
|
_ = a.saveConfig(filepath.Join(a.dataDir, "config.json"))
|
||||||
writeJSON(w, map[string]any{"ok": true})
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
|||||||
+19
-1
@@ -82,6 +82,18 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS node_protocols (
|
||||||
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
|
protocol_id UUID NOT NULL REFERENCES protocols(id) ON DELETE CASCADE,
|
||||||
|
installed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
port INT NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (node_id, protocol_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_node_protocols_protocol ON node_protocols(protocol_id);
|
||||||
`
|
`
|
||||||
_, err := db.Exec(schema)
|
_, err := db.Exec(schema)
|
||||||
return err
|
return err
|
||||||
@@ -129,7 +141,7 @@ func SeedProtocols(db *sql.DB) error {
|
|||||||
for _, p := range defaults {
|
for _, p := range defaults {
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO protocols (code, name, description, port, enabled, sort_order)
|
INSERT INTO protocols (code, name, description, port, enabled, sort_order)
|
||||||
VALUES ($1, $2, $3, $4, TRUE, $5)
|
VALUES ($1, $2, $3, $4, FALSE, $5)
|
||||||
ON CONFLICT (code) DO NOTHING`,
|
ON CONFLICT (code) DO NOTHING`,
|
||||||
p.Code, p.Name, p.Description, p.Port, p.Sort,
|
p.Code, p.Name, p.Description, p.Port, p.Sort,
|
||||||
)
|
)
|
||||||
@@ -185,6 +197,12 @@ UPDATE protocols SET enabled = NOT enabled, updated_at = NOW() WHERE id = $1`, i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetProtocolEnabled(db *sql.DB, id uuid.UUID, enabled bool) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
UPDATE protocols SET enabled = $2, updated_at = NOW() WHERE id = $1`, id, enabled)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func GetStats(db *sql.DB) (models.DashboardStats, error) {
|
func GetStats(db *sql.DB) (models.DashboardStats, error) {
|
||||||
var s models.DashboardStats
|
var s models.DashboardStats
|
||||||
err := db.QueryRow(`SELECT COUNT(*) FROM protocols`).Scan(&s.ProtocolsTotal)
|
err := db.QueryRow(`SELECT COUNT(*) FROM protocols`).Scan(&s.ProtocolsTotal)
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/orohi/vpn-panel/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListProtocolsDetailed(db *sql.DB) ([]models.Protocol, error) {
|
||||||
|
list, err := ListProtocols(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT np.protocol_id, np.installed, np.enabled,
|
||||||
|
n.id, n.name, n.host, n.status
|
||||||
|
FROM node_protocols np
|
||||||
|
JOIN nodes n ON n.id = np.node_id
|
||||||
|
WHERE np.installed = TRUE OR np.enabled = TRUE
|
||||||
|
ORDER BY n.name ASC`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
byID := map[uuid.UUID]*models.Protocol{}
|
||||||
|
for i := range list {
|
||||||
|
byID[list[i].ID] = &list[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var protocolID, nodeID uuid.UUID
|
||||||
|
var installed, enabled bool
|
||||||
|
var name, host, status string
|
||||||
|
if err := rows.Scan(&protocolID, &installed, &enabled, &nodeID, &name, &host, &status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := byID[protocolID]
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref := models.NodeProtoRef{
|
||||||
|
NodeID: nodeID,
|
||||||
|
NodeName: name,
|
||||||
|
Host: host,
|
||||||
|
Status: models.NodeStatus(status),
|
||||||
|
}
|
||||||
|
if installed {
|
||||||
|
p.InstalledOn = append(p.InstalledOn, ref)
|
||||||
|
}
|
||||||
|
if enabled {
|
||||||
|
p.EnabledOn = append(p.EnabledOn, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListNodeProtocols(db *sql.DB, nodeID uuid.UUID) ([]models.NodeProtocol, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT p.id, p.code, p.name, COALESCE(NULLIF(np.port, 0), p.port),
|
||||||
|
COALESCE(np.installed, FALSE), COALESCE(np.enabled, FALSE),
|
||||||
|
COALESCE(np.updated_at, p.updated_at)
|
||||||
|
FROM protocols p
|
||||||
|
LEFT JOIN node_protocols np ON np.protocol_id = p.id AND np.node_id = $1
|
||||||
|
ORDER BY p.sort_order ASC, p.name ASC`, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var list []models.NodeProtocol
|
||||||
|
for rows.Next() {
|
||||||
|
var np models.NodeProtocol
|
||||||
|
np.NodeID = nodeID
|
||||||
|
if err := rows.Scan(
|
||||||
|
&np.ProtocolID, &np.ProtocolCode, &np.ProtocolName, &np.Port,
|
||||||
|
&np.Installed, &np.Enabled, &np.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list = append(list, np)
|
||||||
|
}
|
||||||
|
return list, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpsertNodeProtocol(db *sql.DB, nodeID, protocolID uuid.UUID, installed, enabled bool, port int) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT INTO node_protocols (node_id, protocol_id, installed, enabled, port, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||||
|
ON CONFLICT (node_id, protocol_id) DO UPDATE SET
|
||||||
|
installed = EXCLUDED.installed,
|
||||||
|
enabled = EXCLUDED.enabled,
|
||||||
|
port = EXCLUDED.port,
|
||||||
|
updated_at = NOW()`, nodeID, protocolID, installed, enabled, port)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetNodeProtocolFlags(db *sql.DB, nodeID, protocolID uuid.UUID, installed, enabled bool) error {
|
||||||
|
var port int
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT COALESCE(NULLIF(np.port, 0), p.port)
|
||||||
|
FROM protocols p
|
||||||
|
LEFT JOIN node_protocols np ON np.protocol_id = p.id AND np.node_id = $1
|
||||||
|
WHERE p.id = $2`, nodeID, protocolID).Scan(&port)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return UpsertNodeProtocol(db, nodeID, protocolID, installed, enabled, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProtocol(db *sql.DB, id uuid.UUID) (*models.Protocol, error) {
|
||||||
|
p := &models.Protocol{}
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT id, code, name, description, port, enabled, sort_order, created_at, updated_at
|
||||||
|
FROM protocols WHERE id = $1`, id).Scan(
|
||||||
|
&p.ID, &p.Code, &p.Name, &p.Description, &p.Port,
|
||||||
|
&p.Enabled, &p.SortOrder, &p.CreatedAt, &p.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnabledProtocolCodesForNode(db *sql.DB, nodeID uuid.UUID) ([]string, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT p.code FROM node_protocols np
|
||||||
|
JOIN protocols p ON p.id = np.protocol_id
|
||||||
|
WHERE np.node_id = $1 AND np.enabled = TRUE
|
||||||
|
ORDER BY p.sort_order`, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var codes []string
|
||||||
|
for rows.Next() {
|
||||||
|
var c string
|
||||||
|
if err := rows.Scan(&c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
codes = append(codes, c)
|
||||||
|
}
|
||||||
|
return codes, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstalledProtocolCodesForNode(db *sql.DB, nodeID uuid.UUID) ([]string, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT p.code FROM node_protocols np
|
||||||
|
JOIN protocols p ON p.id = np.protocol_id
|
||||||
|
WHERE np.node_id = $1 AND np.installed = TRUE
|
||||||
|
ORDER BY p.sort_order`, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var codes []string
|
||||||
|
for rows.Next() {
|
||||||
|
var c string
|
||||||
|
if err := rows.Scan(&c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
codes = append(codes, c)
|
||||||
|
}
|
||||||
|
return codes, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncNodeProtocolsFromAgent updates install/enable flags from agent-reported codes.
|
||||||
|
func SyncNodeProtocolsFromAgent(db *sql.DB, nodeID uuid.UUID, installed, enabled []string) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
installedSet := map[string]bool{}
|
||||||
|
for _, c := range installed {
|
||||||
|
installedSet[c] = true
|
||||||
|
}
|
||||||
|
enabledSet := map[string]bool{}
|
||||||
|
for _, c := range enabled {
|
||||||
|
enabledSet[c] = true
|
||||||
|
installedSet[c] = true // enabled implies installed
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.Query(`SELECT id, code, port FROM protocols`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type proto struct {
|
||||||
|
id uuid.UUID
|
||||||
|
code string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
var all []proto
|
||||||
|
for rows.Next() {
|
||||||
|
var p proto
|
||||||
|
if err := rows.Scan(&p.id, &p.code, &p.port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
all = append(all, p)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
|
||||||
|
for _, p := range all {
|
||||||
|
inst := installedSet[p.code]
|
||||||
|
en := enabledSet[p.code]
|
||||||
|
if !inst && !en {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
UPDATE node_protocols SET installed = FALSE, enabled = FALSE, updated_at = NOW()
|
||||||
|
WHERE node_id = $1 AND protocol_id = $2`, nodeID, p.id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
INSERT INTO node_protocols (node_id, protocol_id, installed, enabled, port, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||||
|
ON CONFLICT (node_id, protocol_id) DO UPDATE SET
|
||||||
|
installed = EXCLUDED.installed,
|
||||||
|
enabled = EXCLUDED.enabled,
|
||||||
|
updated_at = NOW()`, nodeID, p.id, inst, en, p.port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NodeProtocolConfigPayload(db *sql.DB, nodeID uuid.UUID) (map[string]any, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT p.code, np.installed, np.enabled, COALESCE(NULLIF(np.port, 0), p.port)
|
||||||
|
FROM node_protocols np
|
||||||
|
JOIN protocols p ON p.id = np.protocol_id
|
||||||
|
WHERE np.node_id = $1 AND (np.installed = TRUE OR np.enabled = TRUE)
|
||||||
|
ORDER BY p.sort_order`, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var installed, enabled []string
|
||||||
|
var items []map[string]any
|
||||||
|
for rows.Next() {
|
||||||
|
var code string
|
||||||
|
var inst, en bool
|
||||||
|
var port int
|
||||||
|
if err := rows.Scan(&code, &inst, &en, &port); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if inst {
|
||||||
|
installed = append(installed, code)
|
||||||
|
}
|
||||||
|
if en {
|
||||||
|
enabled = append(enabled, code)
|
||||||
|
}
|
||||||
|
items = append(items, map[string]any{
|
||||||
|
"code": code, "installed": inst, "enabled": en, "port": port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if installed == nil {
|
||||||
|
installed = []string{}
|
||||||
|
}
|
||||||
|
if enabled == nil {
|
||||||
|
enabled = []string{}
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"protocols": enabled, // backward-compatible health list
|
||||||
|
"protocols_installed": installed,
|
||||||
|
"protocols_enabled": enabled,
|
||||||
|
"protocol_items": items,
|
||||||
|
}, rows.Err()
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/orohi/vpn-panel/internal/auth"
|
"github.com/orohi/vpn-panel/internal/auth"
|
||||||
@@ -72,7 +71,9 @@ func (s *Server) Routes() http.Handler {
|
|||||||
r.HandleFunc("/admin/nodes/{id}", s.Auth.RequireAuth(s.nodeView)).Methods(http.MethodGet)
|
r.HandleFunc("/admin/nodes/{id}", s.Auth.RequireAuth(s.nodeView)).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/admin/nodes/{id}/install", s.Auth.RequireAuth(s.nodeInstall)).Methods(http.MethodPost)
|
r.HandleFunc("/admin/nodes/{id}/install", s.Auth.RequireAuth(s.nodeInstall)).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/admin/nodes/{id}/check", s.Auth.RequireAuth(s.nodeCheck)).Methods(http.MethodPost)
|
r.HandleFunc("/admin/nodes/{id}/check", s.Auth.RequireAuth(s.nodeCheck)).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/admin/nodes/{id}/sync", s.Auth.RequireAuth(s.nodeSyncProtocols)).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/admin/nodes/{id}/delete", s.Auth.RequireAuth(s.nodeDelete)).Methods(http.MethodPost)
|
r.HandleFunc("/admin/nodes/{id}/delete", s.Auth.RequireAuth(s.nodeDelete)).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/admin/nodes/{id}/protocols/{protocolID}", s.Auth.RequireAuth(s.nodeProtocolAction)).Methods(http.MethodPost)
|
||||||
|
|
||||||
r.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
r.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -150,7 +151,7 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "db error", http.StatusInternalServerError)
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
protocols, err := db.ListProtocols(s.DB)
|
protocols, err := db.ListProtocolsDetailed(s.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "db error", http.StatusInternalServerError)
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -165,31 +166,3 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Active": "dashboard",
|
"Active": "dashboard",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) protocols(w http.ResponseWriter, r *http.Request) {
|
|
||||||
protocols, err := db.ListProtocols(s.DB)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "db error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.render(w, "protocols.html", pageData{
|
|
||||||
"Title": "Протоколы",
|
|
||||||
"UserName": s.Auth.CurrentName(r),
|
|
||||||
"Protocols": protocols,
|
|
||||||
"Active": "protocols",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) toggleProtocol(w http.ResponseWriter, r *http.Request) {
|
|
||||||
idStr := mux.Vars(r)["id"]
|
|
||||||
id, err := uuid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := db.ToggleProtocol(s.DB, id); err != nil {
|
|
||||||
http.Error(w, "db error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, "/admin/protocols", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -148,15 +148,21 @@ func (s *Server) nodeView(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
nodeProtocols, err := db.ListNodeProtocols(s.DB, id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
s.render(w, "node_view.html", pageData{
|
s.render(w, "node_view.html", pageData{
|
||||||
"Title": n.Name,
|
"Title": n.Name,
|
||||||
"UserName": s.Auth.CurrentName(r),
|
"UserName": s.Auth.CurrentName(r),
|
||||||
"Active": "nodes",
|
"Active": "nodes",
|
||||||
"Node": n,
|
"Node": n,
|
||||||
"Compose": nodeinstall.ComposeYAML(n),
|
"NodeProtocols": nodeProtocols,
|
||||||
"ManualScript": nodeinstall.ManualInstallScript(n),
|
"Compose": nodeinstall.ComposeYAML(n),
|
||||||
"Flash": r.URL.Query().Get("ok"),
|
"ManualScript": nodeinstall.ManualInstallScript(n),
|
||||||
"Error": r.URL.Query().Get("err"),
|
"Flash": r.URL.Query().Get("ok"),
|
||||||
|
"Error": r.URL.Query().Get("err"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +203,10 @@ func (s *Server) nodeCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = db.MarkNodeSeen(s.DB, id, hr.Version, models.NodeStatusOnline)
|
_ = db.MarkNodeSeen(s.DB, id, hr.Version, models.NodeStatusOnline)
|
||||||
|
// Panel is source of truth — push desired protocols to the agent.
|
||||||
|
if err := s.syncNodeConfig(n); err != nil {
|
||||||
|
log.Printf("push config to node %s: %v", id, err)
|
||||||
|
}
|
||||||
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?ok=online", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?ok=online", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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/nodeclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) syncNodeConfig(n *models.Node) error {
|
||||||
|
cfg, err := db.NodeProtocolConfigPayload(s.DB, n.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nodeclient.PushConfig(n, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) protocols(w http.ResponseWriter, r *http.Request) {
|
||||||
|
protocols, err := db.ListProtocolsDetailed(s.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes, _ := db.ListNodes(s.DB)
|
||||||
|
s.render(w, "protocols.html", pageData{
|
||||||
|
"Title": "Протоколы",
|
||||||
|
"UserName": s.Auth.CurrentName(r),
|
||||||
|
"Protocols": protocols,
|
||||||
|
"Nodes": nodes,
|
||||||
|
"Active": "protocols",
|
||||||
|
"Flash": r.URL.Query().Get("ok"),
|
||||||
|
"Error": r.URL.Query().Get("err"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) toggleProtocol(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := mux.Vars(r)["id"]
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.ToggleProtocol(s.DB, id); err != nil {
|
||||||
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/admin/protocols", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) nodeProtocolAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
nodeID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid node id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
protocolID, err := uuid.Parse(mux.Vars(r)["protocolID"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid protocol id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := r.FormValue("action")
|
||||||
|
if action == "" {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
action = r.FormValue("action")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := db.GetNode(s.DB, nodeID)
|
||||||
|
if err != nil || n == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, err := db.GetProtocol(s.DB, protocolID)
|
||||||
|
if err != nil || p == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := db.ListNodeProtocols(s.DB, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var current models.NodeProtocol
|
||||||
|
for _, item := range list {
|
||||||
|
if item.ProtocolID == protocolID {
|
||||||
|
current = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, enabled := current.Installed, current.Enabled
|
||||||
|
switch action {
|
||||||
|
case "install":
|
||||||
|
installed, enabled = true, true
|
||||||
|
case "uninstall":
|
||||||
|
installed, enabled = false, false
|
||||||
|
case "enable":
|
||||||
|
installed, enabled = true, true
|
||||||
|
case "disable":
|
||||||
|
enabled = false
|
||||||
|
if !installed {
|
||||||
|
installed = false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=bad_action", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.SetNodeProtocolFlags(s.DB, nodeID, protocolID, installed, enabled); err != nil {
|
||||||
|
log.Printf("set node protocol: %v", err)
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=proto_save", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If panel catalog was off and we install/enable — turn protocol on in catalog.
|
||||||
|
if (action == "install" || action == "enable") && !p.Enabled {
|
||||||
|
_ = db.SetProtocolEnabled(s.DB, protocolID, true)
|
||||||
|
}
|
||||||
|
if action == "uninstall" {
|
||||||
|
// If nowhere else has it installed, turn catalog off.
|
||||||
|
detailed, _ := db.ListProtocolsDetailed(s.DB)
|
||||||
|
for _, item := range detailed {
|
||||||
|
if item.ID == protocolID && len(item.InstalledOn) == 0 {
|
||||||
|
_ = db.SetProtocolEnabled(s.DB, protocolID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Status == models.NodeStatusOnline {
|
||||||
|
if err := s.syncNodeConfig(n); err != nil {
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=sync_failed", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=protocol_"+action, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) nodeSyncProtocols(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
|
||||||
|
}
|
||||||
|
n, err := db.GetNode(s.DB, nodeID)
|
||||||
|
if err != nil || n == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hr, err := nodeclient.Health(n)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.UpdateNodeStatus(s.DB, nodeID, models.NodeStatusOffline, err.Error())
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=offline", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = db.MarkNodeSeen(s.DB, nodeID, hr.Version, models.NodeStatusOnline)
|
||||||
|
// Push panel → node (desired state). Do not wipe local DB from empty agent.
|
||||||
|
if err := s.syncNodeConfig(n); err != nil {
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=sync_failed", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=synced", http.StatusSeeOther)
|
||||||
|
}
|
||||||
@@ -25,8 +25,40 @@ type Protocol struct {
|
|||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
InstalledOn []NodeProtoRef `json:"installed_on,omitempty"`
|
||||||
|
EnabledOn []NodeProtoRef `json:"enabled_on,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeProtoRef struct {
|
||||||
|
NodeID uuid.UUID `json:"node_id"`
|
||||||
|
NodeName string `json:"node_name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Status NodeStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeProtocol struct {
|
||||||
|
NodeID uuid.UUID `json:"node_id"`
|
||||||
|
ProtocolID uuid.UUID `json:"protocol_id"`
|
||||||
|
ProtocolCode string `json:"protocol_code"`
|
||||||
|
ProtocolName string `json:"protocol_name"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Protocol) EnabledLabel() string {
|
||||||
|
if p.Enabled {
|
||||||
|
return "Включён"
|
||||||
|
}
|
||||||
|
return "Выключен"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Protocol) InstallCount() int { return len(p.InstalledOn) }
|
||||||
|
func (p Protocol) ActiveCount() int { return len(p.EnabledOn) }
|
||||||
|
|
||||||
|
|
||||||
type NodeStatus string
|
type NodeStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package nodeclient
|
package nodeclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,11 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HealthResponse struct {
|
type HealthResponse struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Uptime int64 `json:"uptime_sec"`
|
Uptime int64 `json:"uptime_sec"`
|
||||||
Protocols []string `json:"protocols"`
|
Protocols []string `json:"protocols"`
|
||||||
|
ProtocolsInstalled []string `json:"protocols_installed"`
|
||||||
|
ProtocolsEnabled []string `json:"protocols_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Health(n *models.Node) (*HealthResponse, error) {
|
func Health(n *models.Node) (*HealthResponse, error) {
|
||||||
@@ -40,5 +43,38 @@ func Health(n *models.Node) (*HealthResponse, error) {
|
|||||||
if err := json.Unmarshal(body, &hr); err != nil {
|
if err := json.Unmarshal(body, &hr); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Fallback: older agents only expose protocols as enabled list.
|
||||||
|
if len(hr.ProtocolsEnabled) == 0 && len(hr.Protocols) > 0 {
|
||||||
|
hr.ProtocolsEnabled = hr.Protocols
|
||||||
|
}
|
||||||
|
if len(hr.ProtocolsInstalled) == 0 && len(hr.ProtocolsEnabled) > 0 {
|
||||||
|
hr.ProtocolsInstalled = append([]string{}, hr.ProtocolsEnabled...)
|
||||||
|
}
|
||||||
return &hr, nil
|
return &hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PushConfig(n *models.Node, cfg map[string]any) error {
|
||||||
|
payload, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("http://%s:%d/api/v1/config", n.Host, n.NodePort)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+n.SecretKey)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 12 * time.Second}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("status %d: %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+31
-2
@@ -274,8 +274,37 @@ input:focus {
|
|||||||
padding: 0.25rem 0.55rem;
|
padding: 0.25rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
.badge.on { background: rgba(61,214,198,0.15); color: var(--ok); }
|
.chip-list {
|
||||||
.badge.off { background: rgba(122,135,156,0.18); color: var(--off); }
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(122,135,156,0.16);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.chip.on { background: rgba(61,214,198,0.15); color: var(--ok); border-color: rgba(61,214,198,0.3); }
|
||||||
|
.chip.warn { background: rgba(255,196,87,0.14); color: #ffc457; }
|
||||||
|
.chip.err { background: rgba(255,107,122,0.14); color: #ff8a96; }
|
||||||
|
.chip.off { opacity: 0.85; }
|
||||||
|
|
||||||
|
.actions-multi {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.row-off { opacity: 0.78; }
|
||||||
|
.proto-servers { margin-top: 0.75rem; }
|
||||||
|
.proto-servers .stat-label { margin-bottom: 0.35rem; }
|
||||||
|
|
||||||
.badge.warn { background: rgba(255, 196, 87, 0.16); color: #ffc457; }
|
.badge.warn { background: rgba(255, 196, 87, 0.16); color: #ffc457; }
|
||||||
.badge.err { background: rgba(255,107,122,0.15); color: #ff8a96; }
|
.badge.err { background: rgba(255,107,122,0.15); color: #ff8a96; }
|
||||||
|
|
||||||
|
|||||||
@@ -88,13 +88,25 @@
|
|||||||
<article class="proto-card {{if .Enabled}}is-on{{else}}is-off{{end}}">
|
<article class="proto-card {{if .Enabled}}is-on{{else}}is-off{{end}}">
|
||||||
<div class="proto-top">
|
<div class="proto-top">
|
||||||
<h3>{{.Name}}</h3>
|
<h3>{{.Name}}</h3>
|
||||||
<span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{if .Enabled}}ON{{else}}OFF{{end}}</span>
|
<span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{.EnabledLabel}}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{{.Description}}</p>
|
<p>{{.Description}}</p>
|
||||||
<div class="proto-meta">
|
<div class="proto-meta">
|
||||||
<code>{{.Code}}</code>
|
<code>{{.Code}}</code>
|
||||||
<span>порт {{.Port}}</span>
|
<span>порт {{.Port}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="proto-servers">
|
||||||
|
{{if .InstalledOn}}
|
||||||
|
<span class="stat-label">Серверы</span>
|
||||||
|
<div class="chip-list">
|
||||||
|
{{range .InstalledOn}}
|
||||||
|
<a class="chip {{statusClass (printf "%s" .Status)}}" href="/admin/nodes/{{.NodeID}}">{{.NodeName}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<span class="muted">Не установлен ни на одной ноде</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,12 +46,69 @@
|
|||||||
<form method="POST" action="/admin/nodes/{{.Node.ID}}/check">
|
<form method="POST" action="/admin/nodes/{{.Node.ID}}/check">
|
||||||
<button class="btn btn-ghost" type="submit">Проверить online</button>
|
<button class="btn btn-ghost" type="submit">Проверить online</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" action="/admin/nodes/{{.Node.ID}}/sync">
|
||||||
|
<button class="btn btn-ghost" type="submit">Синхронизировать протоколы</button>
|
||||||
|
</form>
|
||||||
<form method="POST" action="/admin/nodes/{{.Node.ID}}/delete" onsubmit="return confirm('Удалить ноду из панели?');">
|
<form method="POST" action="/admin/nodes/{{.Node.ID}}/delete" onsubmit="return confirm('Удалить ноду из панели?');">
|
||||||
<button class="btn btn-ghost" type="submit">Удалить</button>
|
<button class="btn btn-ghost" type="submit">Удалить</button>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn btn-ghost" href="/admin/nodes">К списку</a>
|
<a class="btn btn-ghost" href="/admin/nodes">К списку</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="panel-section">
|
||||||
|
<h2>Протоколы на этом сервере</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Протокол</th>
|
||||||
|
<th>Порт</th>
|
||||||
|
<th>Установлен</th>
|
||||||
|
<th>Включён</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .NodeProtocols}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{.ProtocolName}}</strong>
|
||||||
|
<div><code>{{.ProtocolCode}}</code></div>
|
||||||
|
</td>
|
||||||
|
<td>{{.Port}}</td>
|
||||||
|
<td><span class="badge {{if .Installed}}on{{else}}off{{end}}">{{if .Installed}}Да{{else}}Нет{{end}}</span></td>
|
||||||
|
<td><span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{if .Enabled}}ON{{else}}OFF{{end}}</span></td>
|
||||||
|
<td class="actions actions-multi">
|
||||||
|
{{if not .Installed}}
|
||||||
|
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
|
||||||
|
<input type="hidden" name="action" value="install">
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Установить</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
{{if .Enabled}}
|
||||||
|
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
|
||||||
|
<input type="hidden" name="action" value="disable">
|
||||||
|
<button class="btn btn-sm btn-ghost" type="submit">Выключить</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
|
||||||
|
<input type="hidden" name="action" value="enable">
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Включить</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
|
||||||
|
<input type="hidden" name="action" value="uninstall">
|
||||||
|
<button class="btn btn-sm btn-ghost" type="submit">Снять</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Важно</h2>
|
<h2>Важно</h2>
|
||||||
<p class="muted">Откройте порт <strong>{{.Node.NodePort}}</strong> на ноде <em>только</em> для IP панели (как NODE_PORT в Remnawave).</p>
|
<p class="muted">Откройте порт <strong>{{.Node.NodePort}}</strong> на ноде <em>только</em> для IP панели (как NODE_PORT в Remnawave).</p>
|
||||||
|
|||||||
@@ -26,31 +26,57 @@
|
|||||||
<main class="admin-main">
|
<main class="admin-main">
|
||||||
<header class="admin-header">
|
<header class="admin-header">
|
||||||
<h1>Протоколы</h1>
|
<h1>Протоколы</h1>
|
||||||
<p class="muted">Включение и отключение VPN-протоколов</p>
|
<p class="muted">Статус в панели и на каких серверах протокол установлен / включён</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{{if .Flash}}<div class="alert ok">{{.Flash}}</div>{{end}}
|
||||||
|
{{if .Error}}<div class="alert">{{.Error}}</div>{{end}}
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Протокол</th>
|
<th>Протокол</th>
|
||||||
<th>Код</th>
|
|
||||||
<th>Порт</th>
|
<th>Порт</th>
|
||||||
<th>Статус</th>
|
<th>В панели</th>
|
||||||
|
<th>Установлен на</th>
|
||||||
|
<th>Включён на</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Protocols}}
|
{{range .Protocols}}
|
||||||
<tr>
|
<tr class="{{if .Enabled}}row-on{{else}}row-off{{end}}">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{.Name}}</strong>
|
<strong>{{.Name}}</strong>
|
||||||
<div class="cell-desc">{{.Description}}</div>
|
<div class="cell-desc">{{.Description}}</div>
|
||||||
|
<code>{{.Code}}</code>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{{.Code}}</code></td>
|
|
||||||
<td>{{.Port}}</td>
|
<td>{{.Port}}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{if .Enabled}}Включён{{else}}Выключен{{end}}</span>
|
<span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{.EnabledLabel}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .InstalledOn}}
|
||||||
|
<div class="chip-list">
|
||||||
|
{{range .InstalledOn}}
|
||||||
|
<a class="chip {{statusClass (printf "%s" .Status)}}" href="/admin/nodes/{{.NodeID}}" title="{{.Host}}">{{.NodeName}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<span class="muted">нигде</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .EnabledOn}}
|
||||||
|
<div class="chip-list">
|
||||||
|
{{range .EnabledOn}}
|
||||||
|
<a class="chip on" href="/admin/nodes/{{.NodeID}}" title="{{.Host}}">{{.NodeName}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<span class="muted">нигде</span>
|
||||||
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<form method="POST" action="/admin/protocols/{{.ID}}/toggle">
|
<form method="POST" action="/admin/protocols/{{.ID}}/toggle">
|
||||||
@@ -64,6 +90,11 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="hint" style="margin-top:1rem">
|
||||||
|
Установка на сервер — на странице ноды. «В панели» = доступен в каталоге;
|
||||||
|
«Установлен / Включён на» = фактическое состояние на серверах.
|
||||||
|
</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user