package db import ( "database/sql" "fmt" "log" "time" "github.com/google/uuid" _ "github.com/lib/pq" "golang.org/x/crypto/bcrypt" "github.com/orohi/vpn-panel/internal/config" "github.com/orohi/vpn-panel/internal/models" ) func Connect(cfg *config.Config) (*sql.DB, error) { var db *sql.DB var err error for i := 1; i <= 30; i++ { db, err = sql.Open("postgres", cfg.DatabaseURL) if err == nil { err = db.Ping() } if err == nil { db.SetMaxOpenConns(20) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(time.Hour) log.Println("connected to postgres") return db, nil } log.Printf("waiting for postgres (%d/30): %v", i, err) time.Sleep(2 * time.Second) } return nil, fmt.Errorf("postgres unavailable: %w", err) } func Migrate(db *sql.DB) error { schema := ` CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', role TEXT NOT NULL DEFAULT 'admin', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS protocols ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', port INT NOT NULL DEFAULT 0, enabled BOOLEAN NOT NULL DEFAULT TRUE, sort_order INT NOT NULL DEFAULT 0, 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 '', runtime_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); 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); ` if _, err := db.Exec(schema); err != nil { return err } // Additive migrations for existing deployments. _, _ = db.Exec(`ALTER TABLE nodes ADD COLUMN IF NOT EXISTS runtime_log TEXT NOT NULL DEFAULT ''`) return nil } func SeedAdmin(db *sql.DB, cfg *config.Config) error { var exists bool err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)`, cfg.AdminEmail).Scan(&exists) if err != nil { return err } if exists { return nil } hash, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcrypt.DefaultCost) if err != nil { return err } _, err = db.Exec( `INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, 'admin')`, cfg.AdminEmail, string(hash), cfg.AdminName, ) if err == nil { log.Printf("admin seeded: %s", cfg.AdminEmail) } return err } func SeedProtocols(db *sql.DB) error { defaults := []struct { Code, Name, Description string Port, Sort int }{ {"wireguard", "WireGuard", "Современный быстрый VPN-протокол на основе Noise", 51820, 1}, {"openvpn", "OpenVPN", "Классический VPN через UDP/TCP с TLS", 1194, 2}, {"vless", "VLESS", "Лёгкий протокол Xray без шифрования на уровне протокола", 443, 3}, {"vmess", "VMess", "Протокол V2Ray/Xray с обфускацией трафика", 443, 4}, {"trojan", "Trojan", "Трафик маскируется под HTTPS", 443, 5}, {"shadowsocks", "Shadowsocks", "SOCKS5-прокси с шифрованием AEAD", 8388, 6}, {"hysteria2", "Hysteria2", "UDP-протокол на базе QUIC для нестабильных сетей", 443, 7}, } for _, p := range defaults { _, err := db.Exec(` INSERT INTO protocols (code, name, description, port, enabled, sort_order) VALUES ($1, $2, $3, $4, FALSE, $5) ON CONFLICT (code) DO NOTHING`, p.Code, p.Name, p.Description, p.Port, p.Sort, ) if err != nil { return err } } return nil } func GetUserByEmail(db *sql.DB, email string) (*models.User, error) { u := &models.User{} err := db.QueryRow(` SELECT id, email, password_hash, name, role, created_at FROM users WHERE email = $1`, email).Scan( &u.ID, &u.Email, &u.PasswordHash, &u.Name, &u.Role, &u.CreatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return u, nil } func ListProtocols(db *sql.DB) ([]models.Protocol, error) { rows, err := db.Query(` SELECT id, code, name, description, port, enabled, sort_order, created_at, updated_at FROM protocols ORDER BY sort_order ASC, name ASC`) if err != nil { return nil, err } defer rows.Close() var list []models.Protocol for rows.Next() { var p models.Protocol if err := rows.Scan( &p.ID, &p.Code, &p.Name, &p.Description, &p.Port, &p.Enabled, &p.SortOrder, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, err } list = append(list, p) } return list, rows.Err() } func ToggleProtocol(db *sql.DB, id uuid.UUID) error { _, err := db.Exec(` UPDATE protocols SET enabled = NOT enabled, updated_at = NOW() WHERE id = $1`, id) 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) { var s models.DashboardStats err := db.QueryRow(`SELECT COUNT(*) FROM protocols`).Scan(&s.ProtocolsTotal) if err != nil { return s, err } err = db.QueryRow(`SELECT COUNT(*) FROM protocols WHERE enabled = TRUE`).Scan(&s.ProtocolsEnabled) if err != nil { 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 }