ファイル
dhcp-dns-manager/internal/web/server.go
T
CNBUGS AI 7d54c165a9 Fix DNS record and static binding deletion
- Implement actual deletion in handleDeleteRecord (was just returning success)
- Implement actual deletion in handleDeleteBinding (was just returning success)
- Add proper ID validation and error handling
- Check rows affected and return 404 if not found
2026-04-24 17:42:33 +08:00

430 行
11 KiB
Go

package web
import (
"fmt"
"dhcp-dns-manager/internal/config"
"dhcp-dns-manager/internal/db"
"dhcp-dns-manager/internal/dhcp"
"dhcp-dns-manager/internal/dns"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
type Server struct {
config *config.WebConfig
db *db.DB
dhcpServer *dhcp.Server
dnsServer *dns.Server
router *gin.Engine
configManager *ConfigManager
}
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex"`
Password string
IsAdmin bool
}
// Session represents a user session
type Session struct {
ID string `gorm:"primaryKey"`
UserID string // username
ExpiresAt time.Time
CreatedAt time.Time
}
func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Server, cm *ConfigManager) *Server {
gin.SetMode(gin.ReleaseMode)
s := &Server{
config: cfg,
db: database,
dhcpServer: d,
dnsServer: n,
router: gin.New(),
configManager: cm,
}
// Auto-migrate Session table
s.db.AutoMigrate(&Session{})
// Wire up config reloader so DHCP server picks up web UI config changes
d.SetConfigReloader(func() *config.DHCPConfig {
cfg := cm.GetConfig()
dhcpCfg := new(config.DHCPConfig)
*dhcpCfg = cfg.DHCP // copy the value
return dhcpCfg
})
s.setupRoutes()
// Start session cleanup goroutine
go s.cleanupSessions()
return s
}
func (s *Server) cleanupSessions() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
s.db.Where("expires_at < ?", time.Now()).Delete(&Session{})
}
}
func (s *Server) setupRoutes() {
// Custom recovery middleware that returns JSON
s.router.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Internal server error: %v", err),
})
c.Abort()
}))
s.router.Use(gin.Logger())
// Inject ConfigManager into context
s.router.Use(func(c *gin.Context) {
c.Set("configManager", s.configManager)
c.Next()
})
// Static files
s.router.Static("/static", "./web/static")
// Public routes
s.router.GET("/", s.handleIndex)
s.router.POST("/api/login", s.handleLogin)
s.router.GET("/api/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Server is running"})
})
s.router.POST("/api/session/verify", s.handleVerifySession)
// Protected routes
protected := s.router.Group("/api")
protected.Use(s.authMiddleware())
{
// Dashboard
protected.GET("/dashboard", s.handleDashboard)
// DHCP
protected.GET("/dhcp/config", s.handleGetDHCPConfig)
protected.PUT("/dhcp/config", s.handleUpdateDHCPConfig)
protected.GET("/dhcp/leases", s.handleGetLeases)
protected.GET("/dhcp/bindings", s.handleGetBindings)
protected.POST("/dhcp/bindings", s.handleCreateBinding)
protected.DELETE("/dhcp/bindings/:id", s.handleDeleteBinding)
protected.POST("/dhcp/leases/evict", s.handleEvictClient)
// DNS
protected.GET("/dns/config", s.handleGetDNSConfig)
protected.PUT("/dns/config", s.handleUpdateDNSConfig)
protected.GET("/dns/records", s.handleGetRecords)
protected.POST("/dns/records", s.handleCreateRecord)
protected.DELETE("/dns/records/:id", s.handleDeleteRecord)
protected.GET("/dns/logs", s.handleGetLogs)
protected.GET("/dns/zones", s.handleGetZones)
protected.POST("/dns/zones", s.handleCreateZone)
protected.DELETE("/dns/zones/:id", s.handleDeleteZone)
// Config
protected.GET("/config", s.handleGetFullConfig)
protected.PUT("/config", s.handleUpdateConfig)
protected.GET("/config/export", s.handleExportConfig)
protected.POST("/config/import", s.handleImportConfig)
// Service
protected.POST("/service/restart", s.handleRestartService)
}
}
func (s *Server) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
sessionID := c.GetHeader("X-Session-ID")
if sessionID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// Validate session from database
var session Session
if err := s.db.Where("id = ? AND expires_at > ?", sessionID, time.Now()).First(&session).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
c.Abort()
return
}
c.Next()
}
}
func (s *Server) Start() error {
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
return s.router.Run(addr)
}
// Handlers
func (s *Server) handleIndex(c *gin.Context) {
c.File("./web/templates/index.html")
}
func (s *Server) handleLogin(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// TODO: Validate against database
// For now, simple demo auth
if req.Username == "admin" && req.Password == "admin" {
sessionID := fmt.Sprintf("session-%d-%s", time.Now().UnixNano(), req.Username)
session := Session{
ID: sessionID,
UserID: req.Username,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
}
s.db.Create(&session)
c.JSON(http.StatusOK, gin.H{
"session_id": sessionID,
"is_admin": true,
})
return
}
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
func (s *Server) handleVerifySession(c *gin.Context) {
var req struct {
SessionID string `json:"session_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var session Session
if err := s.db.Where("id = ? AND expires_at > ?", req.SessionID, time.Now()).First(&session).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
return
}
c.JSON(http.StatusOK, gin.H{
"valid": true,
"user_id": session.UserID,
"username": session.UserID,
})
}
func (s *Server) handleDashboard(c *gin.Context) {
leases := s.dhcpServer.GetLeases()
bindings, _ := s.dhcpServer.GetStaticBindings()
records, _ := s.dnsServer.GetDNSRecords()
c.JSON(http.StatusOK, gin.H{
"active_leases": len(leases),
"static_bindings": len(bindings),
"dns_records": len(records),
"leases": leases,
"bindings": bindings,
"records": records,
})
}
func (s *Server) handleGetLeases(c *gin.Context) {
leases := s.dhcpServer.GetLeases()
c.JSON(http.StatusOK, gin.H{"leases": leases})
}
func (s *Server) handleGetBindings(c *gin.Context) {
bindings, err := s.dhcpServer.GetStaticBindings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"bindings": bindings})
}
func (s *Server) handleCreateBinding(c *gin.Context) {
var req struct {
MAC string `json:"mac"`
IP string `json:"ip"`
Hostname string `json:"hostname"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := s.dhcpServer.CreateStaticBinding(req.MAC, req.IP, req.Hostname, req.Description); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Binding created"})
}
func (s *Server) handleDeleteBinding(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"})
return
}
// Delete from database
result := s.db.Where("id = ?", id).Delete(&db.DHCPStaticBinding{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Binding not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Binding deleted successfully"})
}
func (s *Server) handleEvictClient(c *gin.Context) {
var req struct {
MAC string `json:"mac"`
IP string `json:"ip"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.MAC == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "MAC is required"})
return
}
if err := s.dhcpServer.EvictClient(req.MAC); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Client evicted successfully"})
}
func (s *Server) handleGetRecords(c *gin.Context) {
records, err := s.dnsServer.GetDNSRecords()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"records": records})
}
func (s *Server) handleCreateRecord(c *gin.Context) {
var req struct {
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int `json:"ttl"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := s.dnsServer.CreateDNSRecord(req.Name, req.Type, req.Value, req.TTL); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Record created"})
}
func (s *Server) handleDeleteRecord(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"})
return
}
// Delete from database
result := s.db.Where("id = ?", id).Delete(&db.DNSRecord{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Record deleted successfully"})
}
func (s *Server) handleGetLogs(c *gin.Context) {
logs, err := s.dnsServer.GetQueryLogs(100)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"logs": logs})
}
func (s *Server) handleGetZones(c *gin.Context) {
zones, err := s.dnsServer.GetDNSZones()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"zones": zones})
}
func (s *Server) handleCreateZone(c *gin.Context) {
var req struct {
Name string `json:"name"`
Type string `json:"type"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := s.dnsServer.CreateDNSZone(req.Name, req.Type); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Zone created"})
}
func (s *Server) handleDeleteZone(c *gin.Context) {
_ = c.Param("id")
// TODO: Convert to uint and delete
c.JSON(http.StatusOK, gin.H{"message": "Zone deleted"})
}
func (s *Server) handleGetConfig(c *gin.Context) {
// Return current config (without sensitive data)
c.JSON(http.StatusOK, gin.H{"config": "placeholder"})
}
func (s *Server) handleUpdateConfig(c *gin.Context) {
// Update config
c.JSON(http.StatusOK, gin.H{"message": "Config updated"})
}