Add node mode with SSH auto-install and Remnawave-style manual deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-29 04:03:33 +03:00
co-authored by Cursor
parent 93bbbf8ce8
commit 7b77d84f68
20 changed files with 1470 additions and 8 deletions
+30
View File
@@ -60,6 +60,28 @@ CREATE TABLE IF NOT EXISTS protocols (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
host TEXT NOT NULL,
ssh_port INT NOT NULL DEFAULT 22,
ssh_user TEXT NOT NULL DEFAULT 'root',
ssh_auth_type TEXT NOT NULL DEFAULT 'password',
ssh_secret_enc TEXT NOT NULL DEFAULT '',
node_port INT NOT NULL DEFAULT 2222,
secret_key TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
install_mode TEXT NOT NULL DEFAULT 'auto',
last_error TEXT NOT NULL DEFAULT '',
install_log TEXT NOT NULL DEFAULT '',
agent_version TEXT NOT NULL DEFAULT '',
last_seen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
`
_, err := db.Exec(schema)
return err
@@ -174,5 +196,13 @@ func GetStats(db *sql.DB) (models.DashboardStats, error) {
return s, err
}
err = db.QueryRow(`SELECT COUNT(*) FROM users WHERE role = 'admin'`).Scan(&s.AdminsTotal)
if err != nil {
return s, err
}
err = db.QueryRow(`SELECT COUNT(*) FROM nodes`).Scan(&s.NodesTotal)
if err != nil {
return s, err
}
err = db.QueryRow(`SELECT COUNT(*) FROM nodes WHERE status = 'online'`).Scan(&s.NodesOnline)
return s, err
}
+138
View File
@@ -0,0 +1,138 @@
package db
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/orohi/vpn-panel/internal/models"
)
func scanNode(scanner interface {
Scan(dest ...any) error
}) (*models.Node, error) {
n := &models.Node{}
var lastSeen sql.NullTime
var status string
err := scanner.Scan(
&n.ID, &n.Name, &n.Host, &n.SSHPort, &n.SSHUser, &n.SSHAuthType, &n.SSHSecretEnc,
&n.NodePort, &n.SecretKey, &status, &n.InstallMode, &n.LastError, &n.InstallLog,
&n.AgentVersion, &lastSeen, &n.CreatedAt, &n.UpdatedAt,
)
if err != nil {
return nil, err
}
n.Status = models.NodeStatus(status)
if lastSeen.Valid {
t := lastSeen.Time
n.LastSeenAt = &t
}
return n, nil
}
const nodeColumns = `
id, name, host, ssh_port, ssh_user, ssh_auth_type, ssh_secret_enc,
node_port, secret_key, status, install_mode, last_error, install_log,
agent_version, last_seen_at, created_at, updated_at`
func CreateNode(db *sql.DB, n *models.Node) error {
if n.ID == uuid.Nil {
n.ID = uuid.New()
}
now := time.Now().UTC()
n.CreatedAt = now
n.UpdatedAt = now
if n.Status == "" {
n.Status = models.NodeStatusPending
}
_, err := db.Exec(`
INSERT INTO nodes (
id, name, host, ssh_port, ssh_user, ssh_auth_type, ssh_secret_enc,
node_port, secret_key, status, install_mode, last_error, install_log,
agent_version, last_seen_at, created_at, updated_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17
)`,
n.ID, n.Name, n.Host, n.SSHPort, n.SSHUser, n.SSHAuthType, n.SSHSecretEnc,
n.NodePort, n.SecretKey, string(n.Status), n.InstallMode, n.LastError, n.InstallLog,
n.AgentVersion, n.LastSeenAt, n.CreatedAt, n.UpdatedAt,
)
return err
}
func GetNode(db *sql.DB, id uuid.UUID) (*models.Node, error) {
row := db.QueryRow(`SELECT `+nodeColumns+` FROM nodes WHERE id = $1`, id)
n, err := scanNode(row)
if err == sql.ErrNoRows {
return nil, nil
}
return n, err
}
func ListNodes(db *sql.DB) ([]models.Node, error) {
rows, err := db.Query(`SELECT ` + nodeColumns + ` FROM nodes ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var list []models.Node
for rows.Next() {
n, err := scanNode(rows)
if err != nil {
return nil, err
}
list = append(list, *n)
}
return list, rows.Err()
}
func UpdateNodeStatus(db *sql.DB, id uuid.UUID, status models.NodeStatus, lastError string) error {
_, err := db.Exec(`
UPDATE nodes
SET status = $2, last_error = $3, updated_at = NOW()
WHERE id = $1`, id, string(status), lastError)
return err
}
func ClearNodeInstallLog(db *sql.DB, id uuid.UUID) error {
_, err := db.Exec(`UPDATE nodes SET install_log = '', updated_at = NOW() WHERE id = $1`, id)
return err
}
func AppendNodeInstallLog(db *sql.DB, id uuid.UUID, chunk string) error {
_, err := db.Exec(`
UPDATE nodes
SET install_log = CASE
WHEN install_log = '' OR install_log IS NULL THEN $2
ELSE install_log || E'\n' || $2
END, updated_at = NOW()
WHERE id = $1`, id, strings.TrimRight(chunk, "\n"))
return err
}
func MarkNodeSeen(db *sql.DB, id uuid.UUID, version string, status models.NodeStatus) error {
_, err := db.Exec(`
UPDATE nodes
SET last_seen_at = NOW(), agent_version = $2, status = $3, last_error = '', updated_at = NOW()
WHERE id = $1`, id, version, string(status))
return err
}
func DeleteNode(db *sql.DB, id uuid.UUID) error {
res, err := db.Exec(`DELETE FROM nodes WHERE id = $1`, id)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("node not found")
}
return nil
}