114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
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()
|
|
}
|
|
} |