Forráskód Böngészése

Implement persistent sessions with 30-day expiration

- Move session storage from in-memory to database
- Add Session model and auto-migrate table
- Set session expiration to 30 days
- Add /api/session/verify endpoint for frontend validation
- Add background session cleanup task (hourly)
- Frontend now verifies session validity on page load
- Clear localStorage when session expires
CNBUGS AI 1 hónapja
szülő
commit
1a0e743a71
2 módosított fájl, 85 hozzáadás és 13 törlés
  1. 58 7
      internal/web/server.go
  2. 27 6
      web/static/js/app.js

+ 58 - 7
internal/web/server.go

@@ -8,7 +8,6 @@ import (
 	"dhcp-dns-manager/internal/dns"
 	"github.com/gin-gonic/gin"
 	"net/http"
-	"sync"
 	"time"
 )
 
@@ -28,6 +27,14 @@ type User struct {
 	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)
 	
@@ -40,6 +47,9 @@ func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Se
 		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()
@@ -49,9 +59,22 @@ func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Se
 	})
 	
 	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) {
@@ -78,6 +101,7 @@ func (s *Server) setupRoutes() {
 	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")
@@ -116,9 +140,6 @@ func (s *Server) setupRoutes() {
 	}
 }
 
-// sessionStore in-memory session store
-var sessionStore = sync.Map{}
-
 func (s *Server) authMiddleware() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		sessionID := c.GetHeader("X-Session-ID")
@@ -128,8 +149,9 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
 			return
 		}
 
-		// Validate session exists in store
-		if _, ok := sessionStore.Load(sessionID); !ok {
+		// 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
@@ -164,7 +186,13 @@ func (s *Server) handleLogin(c *gin.Context) {
 	// For now, simple demo auth
 	if req.Username == "admin" && req.Password == "admin" {
 		sessionID := fmt.Sprintf("session-%d-%s", time.Now().UnixNano(), req.Username)
-		sessionStore.Store(sessionID, true)
+		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,
@@ -175,6 +203,29 @@ func (s *Server) handleLogin(c *gin.Context) {
 	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()

+ 27 - 6
web/static/js/app.js

@@ -4,12 +4,33 @@ let autoRefreshEnabled = false;
 
 // Restore session on page load
 if (sessionId) {
-    document.getElementById('loginSection').style.display = 'none';
-    document.getElementById('dashboard').style.display = 'block';
-    document.getElementById('logoutBtn').style.display = 'block';
-    loadDashboard();
-    loadDHCPConfig();
-    loadDNSConfig();
+    // Verify session is still valid
+    fetch('/api/session/verify', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ session_id: sessionId })
+    })
+    .then(res => res.json())
+    .then(data => {
+        if (data.valid) {
+            document.getElementById('loginSection').style.display = 'none';
+            document.getElementById('dashboard').style.display = 'block';
+            document.getElementById('logoutBtn').style.display = 'block';
+            loadDashboard();
+            loadDHCPConfig();
+            loadDNSConfig();
+        } else {
+            // Session expired, clear and show login
+            localStorage.removeItem('session_id');
+            sessionId = null;
+        }
+    })
+    .catch(err => {
+        console.error('Session verify error:', err);
+        // On error, clear and show login
+        localStorage.removeItem('session_id');
+        sessionId = null;
+    });
 }
 
 // Auto Refresh