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 var profileID uuid.NullUUID var profileName sql.NullString 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.RuntimeLog, &n.AgentVersion, &lastSeen, &n.CreatedAt, &n.UpdatedAt, &profileID, &profileName, ) if err != nil { return nil, err } n.Status = models.NodeStatus(status) if lastSeen.Valid { t := lastSeen.Time n.LastSeenAt = &t } if profileID.Valid { id := profileID.UUID n.ProfileID = &id } if profileName.Valid { n.ProfileName = profileName.String } return n, nil } const nodeColumns = ` n.id, n.name, n.host, n.ssh_port, n.ssh_user, n.ssh_auth_type, n.ssh_secret_enc, n.node_port, n.secret_key, n.status, n.install_mode, n.last_error, n.install_log, n.runtime_log, n.agent_version, n.last_seen_at, n.created_at, n.updated_at, n.profile_id, COALESCE(cp.name, '')` const nodeFrom = ` FROM nodes n LEFT JOIN config_profiles cp ON cp.id = n.profile_id` 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, runtime_log, agent_version, last_seen_at, created_at, updated_at, profile_id ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19 )`, 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.RuntimeLog, n.AgentVersion, n.LastSeenAt, n.CreatedAt, n.UpdatedAt, n.ProfileID, ) return err } func GetNode(db *sql.DB, id uuid.UUID) (*models.Node, error) { row := db.QueryRow(`SELECT `+nodeColumns+nodeFrom+` WHERE n.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 + nodeFrom + ` ORDER BY n.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 SetNodeRuntimeLog(db *sql.DB, id uuid.UUID, logText string) error { const max = 200_000 if len(logText) > max { logText = logText[len(logText)-max:] } _, err := db.Exec(` UPDATE nodes SET runtime_log = $2, updated_at = NOW() WHERE id = $1`, id, logText) 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 SetNodeProfile(db *sql.DB, nodeID uuid.UUID, profileID *uuid.UUID) error { _, err := db.Exec(`UPDATE nodes SET profile_id = $2, updated_at = NOW() WHERE id = $1`, nodeID, profileID) 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 }