7d54c165a9
- 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
430 regels
11 KiB
Go
430 regels
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"})
|
|
}
|