Files
panel-vpn/internal/db/db.go
T

227 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 '',
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);
`
_, err := db.Exec(schema)
return err
}
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
}