package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" applogger "github.com/gochat/gochat/pkg/logger" ) // SessionMiddlewareConfig holds configuration for the session middleware. type SessionMiddlewareConfig struct { SessionHeader string `mapstructure:"session_header"` // default: X-Session-ID SkipPaths []string `mapstructure:"skip_paths"` // paths that skip session validation } // DefaultSessionMiddlewareConfig returns default session middleware configuration. func DefaultSessionMiddlewareConfig() SessionMiddlewareConfig { return SessionMiddlewareConfig{ SessionHeader: "X-Session-ID", SkipPaths: []string{"/api/v1/auth/login", "/api/v1/auth/register", "/api/v1/auth/refresh", "/health"}, } } // SessionMiddlewareConfigFromAppConfig derives SessionMiddlewareConfig from the global Config. func SessionMiddlewareConfigFromAppConfig(cfg *config.Config) SessionMiddlewareConfig { smc := DefaultSessionMiddlewareConfig() if cfg.Session.HeaderName != "" { smc.SessionHeader = cfg.Session.HeaderName } smc.SkipPaths = cfg.Session.SkipPaths return smc } // SessionMiddleware validates and loads user sessions from the SessionStore. // This middleware is complementary to AuthMiddleware: // - AuthMiddleware validates JWT tokens and sets user_id/role/claims // - SessionMiddleware validates the session cookie/header and enriches context // // Session middleware provides: // 1. Session continuity across token refreshes // 2. Force logout capability (delete all sessions for a user) // 3. Session-scoped metadata (active account switching, UI preferences) // // Reference: Chatwoot uses Devise sessions; we replace with JWT + session store. func SessionMiddleware(store *auth.SessionStore, cfg SessionMiddlewareConfig) gin.HandlerFunc { return func(c *gin.Context) { // Skip session validation for configured paths path := c.Request.URL.Path for _, skip := range cfg.SkipPaths { if strings.HasPrefix(path, skip) { c.Next() return } } // Look for session ID in header first (API clients), then cookie (web clients) sessionID := c.GetHeader(cfg.SessionHeader) if sessionID == "" { sessionID, _ = c.Cookie("_gochat_session") } if sessionID == "" { // No session provided — this is acceptable for JWT-only auth. // Session is optional enrichment; JWT remains the primary auth mechanism. c.Next() return } session, err := store.Get(sessionID) if err != nil { // Session not found or expired — clear the stale session cookie c.SetCookie("_gochat_session", "", -1, "/", "", false, true) applogger.L().Debugf("Session lookup failed: %v (path=%s)", err, path) // Don't abort — JWT auth is still valid. Session is enrichment only. c.Next() return } // Set session data in context for downstream handlers c.Set("session_id", session.ID) c.Set("session", session) // Refresh session expiry (keep-alive on activity) if refreshed, err := store.Refresh(session.ID); err == nil { c.Set("session_expires_at", refreshed.ExpiresAt) // Update session cookie expiry c.SetCookie("_gochat_session", session.ID, int(refreshed.ExpiresAt.Sub(refreshed.CreatedAt).Seconds()), "/", "", true, true) } c.Next() } } // RequireSession is a middleware that requires an active session. // Use on routes where session-based features are mandatory (e.g., WebSocket connections // that need session continuity, account switching endpoints). func RequireSession() gin.HandlerFunc { return func(c *gin.Context) { sessionID, exists := c.Get("session_id") if !exists || sessionID == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "active session required", }) return } c.Next() } }