Fix user default surface by reading JWT from cookie

The GET / route has no auth middleware, so user_id was always
empty. Add UserIDFromCookie helper that opportunistically reads
the sb_token cookie and extracts user_id without requiring auth.
Now user preference correctly overrides the global default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 14:00:09 +00:00
parent 7c5998b6ce
commit 2b3a3f08ef
2 changed files with 22 additions and 1 deletions

View File

@@ -142,6 +142,21 @@ func parseAndValidateJWT(tokenString string, jwtSecret string) (*Claims, bool) {
return claims, true return claims, true
} }
// UserIDFromCookie extracts the user ID from the sb_token cookie without
// requiring authentication. Returns "" if no valid token is found.
// Used by unauthenticated routes that want optional user context.
func UserIDFromCookie(c *gin.Context, jwtSecret string) string {
cookie, err := c.Cookie("sb_token")
if err != nil || cookie == "" {
return ""
}
claims, ok := parseAndValidateJWT(cookie, jwtSecret)
if !ok {
return ""
}
return claims.UserID
}
// ─── Auth middleware ───────────────────────────────────────── // ─── Auth middleware ─────────────────────────────────────────
// Auth returns a Gin middleware that validates JWT bearer tokens and // Auth returns a Gin middleware that validates JWT bearer tokens and

View File

@@ -6,6 +6,8 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"switchboard-core/middleware"
) )
// SeedSurfaces writes core surface manifests to the registry table. // SeedSurfaces writes core surface manifests to the registry table.
@@ -92,9 +94,13 @@ func (e *Engine) EnabledSurfaceIDs() []string {
// DefaultSurfaceRedirect returns a handler for GET / that redirects to the // DefaultSurfaceRedirect returns a handler for GET / that redirects to the
// configured default surface, falling back to the first enabled extension // configured default surface, falling back to the first enabled extension
// surface, then /welcome. // surface, then /welcome.
//
// This route has no auth middleware, so we opportunistically read the JWT
// from the cookie to check user preferences. If the cookie is missing or
// invalid, user preference is skipped (falls through to global default).
func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc { func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
userID := c.GetString("user_id") userID := middleware.UserIDFromCookie(c, e.cfg.JWTSecret)
target := e.resolveDefaultSurface(c.Request.Context(), userID) target := e.resolveDefaultSurface(c.Request.Context(), userID)
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+target) c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+target)
} }