package middleware import ( "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "git.gobha.me/xcaliber/chat-switchboard/config" "git.gobha.me/xcaliber/chat-switchboard/database" ) // AuthOrRedirect validates JWT tokens for page routes. // Unlike Auth() which returns 401 JSON for API calls, this redirects // to the login page — appropriate for browser navigation. // // Token is read from the "sb_token" cookie (set by the login page JS) // since page requests don't have Authorization headers. func AuthOrRedirect(cfg *config.Config) gin.HandlerFunc { loginPath := cfg.BasePath + "/login" return func(c *gin.Context) { // Skip auth when running without a database (unmanaged mode) if !database.IsConnected() { c.Next() return } // Try cookie first (set by login page), then Authorization header, // then query param (for edge cases) tokenString := "" if cookie, err := c.Cookie("sb_token"); err == nil && cookie != "" { tokenString = cookie } if tokenString == "" { header := c.GetHeader("Authorization") if strings.HasPrefix(header, "Bearer ") { tokenString = strings.TrimPrefix(header, "Bearer ") } } if tokenString == "" { tokenString = c.Query("token") } if tokenString == "" { redirectToLogin(c, loginPath) return } claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(cfg.JWTSecret), nil }) if err != nil || !token.Valid { redirectToLogin(c, loginPath) return } // Store claims in context (same as Auth middleware) c.Set("user_id", claims.UserID) c.Set("email", claims.Email) c.Set("role", claims.Role) c.Next() } } // RequireAdminPage aborts with 403 if the user isn't an admin. // Use after AuthOrRedirect for admin-only page routes. func RequireAdminPage() gin.HandlerFunc { return func(c *gin.Context) { role, _ := c.Get("role") if role != "admin" { c.String(http.StatusForbidden, "Admin access required") c.Abort() return } c.Next() } } func redirectToLogin(c *gin.Context, loginPath string) { // Save intended destination for post-login redirect intended := c.Request.URL.Path if c.Request.URL.RawQuery != "" { intended += "?" + c.Request.URL.RawQuery } c.SetCookie("redirect_after_login", url.QueryEscape(intended), 300, "/", "", false, true) c.Redirect(http.StatusFound, loginPath) c.Abort() }