336 lines
11 KiB
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"})
|
|
}
|