Files
lan-manager/server/handlers/machines.go

336 lines
11 KiB
Go

package handlers
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"lan-manager/server/db"
"lan-manager/server/middleware"
"lan-manager/server/models"
"lan-manager/server/services"
"lan-manager/server/utils"
)
type MachineHandler struct{}
func NewMachineHandler() *MachineHandler {
return &MachineHandler{}
}
// sanitizeMachine removes sensitive fields for guest users
func sanitizeMachine(m *models.Machine) {
m.IP = ""
m.MAC = models.NullString{}
m.Notes = models.NullString{}
m.SSHUsername = models.NullString{}
m.SSHPassword = models.NullString{}
m.ListenPorts = models.NullString{}
m.CreatedAt = time.Time{}
m.UpdatedAt = time.Time{}
}
func (h *MachineHandler) List(c *gin.Context) {
isAdmin := middleware.IsAdmin(c)
osFilter := c.Query("os_type")
search := c.Query("search")
query := `SELECT id, hostname, ip, mac, os_type, os_version, notes, ssh_port, ssh_username, ssh_password, is_online, last_ping_at, cpu_info, memory_info, disk_info, uptime, listen_ports, ssh_synced_at, created_at, updated_at FROM machines WHERE 1=1`
args := []interface{}{}
if osFilter != "" {
query += ` AND os_type = ?`
args = append(args, osFilter)
}
if search != "" {
if isAdmin {
query += ` AND (hostname LIKE ? OR ip LIKE ?)`
args = append(args, "%"+search+"%", "%"+search+"%")
} else {
query += ` AND hostname LIKE ?`
args = append(args, "%"+search+"%")
}
}
query += ` ORDER BY created_at DESC`
rows, err := db.DB.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
machines := []models.Machine{}
for rows.Next() {
var m models.Machine
var lp, ss *time.Time
err := rows.Scan(&m.ID, &m.Hostname, &m.IP, &m.MAC, &m.OsType, &m.OsVersion, &m.Notes, &m.SSHPort, &m.SSHUsername, &m.SSHPassword, &m.IsOnline, &lp, &m.CPUInfo, &m.MemoryInfo, &m.DiskInfo, &m.Uptime, &m.ListenPorts, &ss, &m.CreatedAt, &m.UpdatedAt)
if err != nil {
continue
}
m.LastPingAt = lp
m.SSHSyncedAt = ss
m.SSHPassword = models.NullString{} // never return password
if !isAdmin {
sanitizeMachine(&m)
}
machines = append(machines, m)
}
rows.Close()
// Fetch service counts
if len(machines) > 0 {
scRows, err := db.DB.Query(`SELECT machine_id, COUNT(*) FROM services GROUP BY machine_id`)
if err == nil {
defer scRows.Close()
for scRows.Next() {
var mid int64
var cnt int
if err := scRows.Scan(&mid, &cnt); err == nil {
for i := range machines {
if machines[i].ID == mid {
machines[i].ServiceCount = cnt
break
}
}
}
}
}
}
c.JSON(http.StatusOK, machines)
}
func (h *MachineHandler) Get(c *gin.Context) {
isAdmin := middleware.IsAdmin(c)
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var m models.Machine
var lp, ss *time.Time
err = db.DB.QueryRow(`SELECT id, hostname, ip, mac, os_type, os_version, notes, ssh_port, ssh_username, ssh_password, is_online, last_ping_at, cpu_info, memory_info, disk_info, uptime, listen_ports, ssh_synced_at, created_at, updated_at FROM machines WHERE id = ?`, id).
Scan(&m.ID, &m.Hostname, &m.IP, &m.MAC, &m.OsType, &m.OsVersion, &m.Notes, &m.SSHPort, &m.SSHUsername, &m.SSHPassword, &m.IsOnline, &lp, &m.CPUInfo, &m.MemoryInfo, &m.DiskInfo, &m.Uptime, &m.ListenPorts, &ss, &m.CreatedAt, &m.UpdatedAt)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
m.LastPingAt = lp
m.SSHSyncedAt = ss
services := []models.Service{}
if rows, err := db.DB.Query(`SELECT id, machine_id, name, port, protocol, notes FROM services WHERE machine_id = ?`, id); err == nil {
defer rows.Close()
for rows.Next() {
var s models.Service
if err := rows.Scan(&s.ID, &s.MachineID, &s.Name, &s.Port, &s.Protocol, &s.Notes); err == nil {
if !isAdmin {
s.Notes = ""
}
services = append(services, s)
}
}
}
relationships := []models.Relationship{}
if isAdmin {
if rows, err := db.DB.Query(`SELECT id, source_machine_id, target_machine_id, relation_type, source_port, target_port, notes FROM relationships WHERE source_machine_id = ? OR target_machine_id = ?`, id, id); err == nil {
defer rows.Close()
for rows.Next() {
var r models.Relationship
if err := rows.Scan(&r.ID, &r.SourceMachineID, &r.TargetMachineID, &r.RelationType, &r.SourcePort, &r.TargetPort, &r.Notes); err == nil {
relationships = append(relationships, r)
}
}
}
}
m.SSHPassword = models.NullString{} // never return password
if !isAdmin {
sanitizeMachine(&m)
}
c.JSON(http.StatusOK, gin.H{
"machine": m,
"services": services,
"relationships": relationships,
})
}
func (h *MachineHandler) Create(c *gin.Context) {
var m models.Machine
if err := c.ShouldBindJSON(&m); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
encPass, _ := utils.Encrypt(m.SSHPassword.String)
passField := models.NullString{}
passField.String = encPass
passField.Valid = encPass != ""
res, err := db.DB.Exec(`INSERT INTO machines (hostname, ip, mac, os_type, os_version, notes, ssh_port, ssh_username, ssh_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
m.Hostname, m.IP, m.MAC, m.OsType, m.OsVersion, m.Notes, m.SSHPort, m.SSHUsername, passField)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
id, _ := res.LastInsertId()
m.ID = id
m.CreatedAt = time.Now()
m.UpdatedAt = time.Now()
middleware.LogOperation("create", "machine", &m.ID, m.Hostname, "", middleware.ToJSON(m), c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusCreated, m)
}
func (h *MachineHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var m models.Machine
if err := c.ShouldBindJSON(&m); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var old models.Machine
var oldPass string
var lp, ss *time.Time
_ = db.DB.QueryRow(`SELECT id, hostname, ip, mac, os_type, os_version, notes, ssh_port, ssh_username, ssh_password, is_online, last_ping_at, cpu_info, memory_info, disk_info, uptime, listen_ports, ssh_synced_at, created_at, updated_at FROM machines WHERE id = ?`, id).
Scan(&old.ID, &old.Hostname, &old.IP, &old.MAC, &old.OsType, &old.OsVersion, &old.Notes, &old.SSHPort, &old.SSHUsername, &oldPass, &old.IsOnline, &lp, &old.CPUInfo, &old.MemoryInfo, &old.DiskInfo, &old.Uptime, &old.ListenPorts, &ss, &old.CreatedAt, &old.UpdatedAt)
old.LastPingAt = lp
old.SSHSyncedAt = ss
// Keep old password if new one is empty
passToSave := oldPass
if m.SSHPassword.String != "" {
encPass, err := utils.Encrypt(m.SSHPassword.String)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"})
return
}
passToSave = encPass
}
_, err = db.DB.Exec(`UPDATE machines SET hostname=?, ip=?, mac=?, os_type=?, os_version=?, notes=?, ssh_port=?, ssh_username=?, ssh_password=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`,
m.Hostname, m.IP, m.MAC, m.OsType, m.OsVersion, m.Notes, m.SSHPort, m.SSHUsername, passToSave, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
m.ID = id
old.SSHPassword = models.NullString{}
m.SSHPassword = models.NullString{}
middleware.LogOperation("update", "machine", &m.ID, m.Hostname, middleware.ToJSON(old), middleware.ToJSON(m), c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, m)
}
func (h *MachineHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var name string
_ = db.DB.QueryRow(`SELECT hostname FROM machines WHERE id = ?`, id).Scan(&name)
_, err = db.DB.Exec(`DELETE FROM machines WHERE id = ?`, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
middleware.LogOperation("delete", "machine", &id, name, "", "", c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *MachineHandler) SSHInfo(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req models.SSHInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var ip string
var sshPort int
var savedUser, savedPass string
if err := db.DB.QueryRow(`SELECT ip, ssh_port, ssh_username, ssh_password FROM machines WHERE id = ?`, id).Scan(&ip, &sshPort, &savedUser, &savedPass); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "machine not found"})
return
}
user := req.Username
pass := req.Password
// 如果前端没传密码,尝试用数据库保存的凭据
if pass == "" {
if savedUser == "" || savedPass == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "未保存 SSH 凭据,请手动输入"})
return
}
plainPass, decErr := utils.Decrypt(savedPass)
if decErr != nil || plainPass == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "未保存 SSH 凭据,请手动输入"})
return
}
user = savedUser
pass = plainPass
}
result, err := services.GetSSHInfo(ip, sshPort, user, pass)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Save credentials to machine record for auto-sync (only when user explicitly provided password)
if req.Password != "" {
encPass, _ := utils.Encrypt(req.Password)
_, _ = db.DB.Exec(`UPDATE machines SET ssh_username=?, ssh_password=? WHERE id=?`, req.Username, encPass, id)
}
c.JSON(http.StatusOK, result)
}
func (h *MachineHandler) SyncSSH(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req models.SSHInfoResult
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
portsStr := ""
if len(req.ListenPorts) > 0 {
b, _ := json.Marshal(req.ListenPorts)
portsStr = string(b)
// remove brackets
if len(portsStr) > 2 {
portsStr = portsStr[1 : len(portsStr)-1]
}
}
_, err = db.DB.Exec(`UPDATE machines SET cpu_info=?, memory_info=?, disk_info=?, uptime=?, listen_ports=?, ssh_synced_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?`,
req.RawCPUInfo, req.RawMemoryInfo, req.RawDiskInfo, req.Uptime, portsStr, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
middleware.LogOperation("update", "machine", &id, "sync-ssh", "", "", c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, gin.H{"message": "synced"})
}