package middleware import ( "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "switchboard-core/auth" "switchboard-core/config" "switchboard-core/database" "switchboard-core/store" ) // 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, users store.UserStore, cache *UserStatusCache) 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, ok := parseAndValidateJWT(tokenString, cfg.JWTSecret) if !ok { redirectToLogin(c, loginPath) return } if claims.UserID == "" { redirectToLogin(c, loginPath) return } // Check user is active — redirect (not JSON) on failure. if entry, hit := cache.get(claims.UserID); hit { if !entry.isActive { redirectToLogin(c, loginPath) return } } else { user, err := users.GetByID(c.Request.Context(), claims.UserID) if err != nil || !user.IsActive { redirectToLogin(c, loginPath) return } cache.set(claims.UserID, user.IsActive) } c.Set("user_id", claims.UserID) c.Set("email", claims.Email) c.Next() } } // RequireAdminPage aborts with 403 if the user lacks surface.admin.access. // Use after AuthOrRedirect for admin-only page routes. func RequireAdminPage(stores store.Stores) gin.HandlerFunc { return func(c *gin.Context) { userID := c.GetString("user_id") perms, err := resolveAndCachePerms(c, stores, userID) if err != nil || !perms[auth.PermSurfaceAdminAccess] { 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() }