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) // 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) { _ = c.Param("id") // TODO: Convert to uint and delete c.JSON(http.StatusOK, gin.H{"message": "Binding deleted"}) } 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) { _ = c.Param("id") // TODO: Convert to uint and delete c.JSON(http.StatusOK, gin.H{"message": "Record deleted"}) } 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"}) }