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
This commit is contained in:
+58
-7
@@ -8,7 +8,6 @@ import (
|
|||||||
"dhcp-dns-manager/internal/dns"
|
"dhcp-dns-manager/internal/dns"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +27,14 @@ type User struct {
|
|||||||
IsAdmin bool
|
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 {
|
func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Server, cm *ConfigManager) *Server {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
@@ -40,6 +47,9 @@ func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Se
|
|||||||
configManager: cm,
|
configManager: cm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-migrate Session table
|
||||||
|
s.db.AutoMigrate(&Session{})
|
||||||
|
|
||||||
// Wire up config reloader so DHCP server picks up web UI config changes
|
// Wire up config reloader so DHCP server picks up web UI config changes
|
||||||
d.SetConfigReloader(func() *config.DHCPConfig {
|
d.SetConfigReloader(func() *config.DHCPConfig {
|
||||||
cfg := cm.GetConfig()
|
cfg := cm.GetConfig()
|
||||||
@@ -49,9 +59,22 @@ func NewServer(cfg *config.WebConfig, database *db.DB, d *dhcp.Server, n *dns.Se
|
|||||||
})
|
})
|
||||||
|
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
|
|
||||||
|
// Start session cleanup goroutine
|
||||||
|
go s.cleanupSessions()
|
||||||
|
|
||||||
return s
|
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() {
|
func (s *Server) setupRoutes() {
|
||||||
// Custom recovery middleware that returns JSON
|
// Custom recovery middleware that returns JSON
|
||||||
s.router.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
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) {
|
s.router.GET("/api/health", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Server is running"})
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Server is running"})
|
||||||
})
|
})
|
||||||
|
s.router.POST("/api/session/verify", s.handleVerifySession)
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
protected := s.router.Group("/api")
|
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 {
|
func (s *Server) authMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
sessionID := c.GetHeader("X-Session-ID")
|
sessionID := c.GetHeader("X-Session-ID")
|
||||||
@@ -128,8 +149,9 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate session exists in store
|
// Validate session from database
|
||||||
if _, ok := sessionStore.Load(sessionID); !ok {
|
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.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
@@ -164,7 +186,13 @@ func (s *Server) handleLogin(c *gin.Context) {
|
|||||||
// For now, simple demo auth
|
// For now, simple demo auth
|
||||||
if req.Username == "admin" && req.Password == "admin" {
|
if req.Username == "admin" && req.Password == "admin" {
|
||||||
sessionID := fmt.Sprintf("session-%d-%s", time.Now().UnixNano(), req.Username)
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"session_id": sessionID,
|
"session_id": sessionID,
|
||||||
"is_admin": true,
|
"is_admin": true,
|
||||||
@@ -175,6 +203,29 @@ func (s *Server) handleLogin(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
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) {
|
func (s *Server) handleDashboard(c *gin.Context) {
|
||||||
leases := s.dhcpServer.GetLeases()
|
leases := s.dhcpServer.GetLeases()
|
||||||
bindings, _ := s.dhcpServer.GetStaticBindings()
|
bindings, _ := s.dhcpServer.GetStaticBindings()
|
||||||
|
|||||||
+27
-6
@@ -4,12 +4,33 @@ let autoRefreshEnabled = false;
|
|||||||
|
|
||||||
// Restore session on page load
|
// Restore session on page load
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
document.getElementById('loginSection').style.display = 'none';
|
// Verify session is still valid
|
||||||
document.getElementById('dashboard').style.display = 'block';
|
fetch('/api/session/verify', {
|
||||||
document.getElementById('logoutBtn').style.display = 'block';
|
method: 'POST',
|
||||||
loadDashboard();
|
headers: { 'Content-Type': 'application/json' },
|
||||||
loadDHCPConfig();
|
body: JSON.stringify({ session_id: sessionId })
|
||||||
loadDNSConfig();
|
})
|
||||||
|
.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
|
// Auto Refresh
|
||||||
|
|||||||
Reference in New Issue
Block a user