package middleware import ( "errors" "net/http" "os" "strings" "clawreef/internal/repository" "clawreef/internal/utils" "github.com/gin-gonic/gin" ) // Auth middleware validates JWT token func Auth() gin.HandlerFunc { return func(c *gin.Context) { tokenString, ok := extractToken(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Authorization header required", }) c.Abort() return } claims, err := validateUserAccessToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Invalid or expired token", }) c.Abort() return } // Check token type if claims.TokenType != "access" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Invalid token type", }) c.Abort() return } // Set user ID in context c.Set("userID", claims.UserID) c.Next() } } // GatewayAuth accepts either a normal user access JWT or an instance lifecycle gateway token. func GatewayAuth(instanceRepo repository.InstanceRepository, bindingRepos ...repository.InstanceRuntimeBindingRepository) gin.HandlerFunc { var bindingRepo repository.InstanceRuntimeBindingRepository if len(bindingRepos) > 0 { bindingRepo = bindingRepos[0] } return func(c *gin.Context) { tokenString, ok := extractToken(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Authorization header required", }) c.Abort() return } if claims, err := validateUserAccessToken(tokenString); err == nil { c.Set("userID", claims.UserID) c.Set("gatewayAuthType", "user") c.Next() return } instance, err := instanceRepo.GetByAccessToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Invalid gateway token", }) c.Abort() return } if instance == nil || instance.AccessToken == nil || strings.TrimSpace(*instance.AccessToken) == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Invalid gateway token", }) c.Abort() return } c.Set("userID", instance.UserID) c.Set("instanceID", instance.ID) c.Set("instanceMode", gatewayInstanceMode(instance.InstanceMode, instance.RuntimeType)) c.Set("runtimeType", strings.TrimSpace(instance.RuntimeType)) if bindingRepo != nil { if binding, err := bindingRepo.GetRunningByInstanceID(c.Request.Context(), instance.ID); err == nil && binding != nil { c.Set("gatewayID", strings.TrimSpace(binding.GatewayID)) c.Set("runtimePodID", binding.RuntimePodID) } } c.Set("gatewayAuthType", "instance") c.Next() } } func gatewayInstanceMode(instanceMode, runtimeType string) string { mode := strings.ToLower(strings.TrimSpace(instanceMode)) if mode == "lite" || mode == "pro" { return mode } switch strings.ToLower(strings.TrimSpace(runtimeType)) { case "gateway": return "lite" case "desktop", "shell": return "pro" default: return mode } } func extractToken(c *gin.Context) (string, bool) { authHeader := c.GetHeader("Authorization") if authHeader != "" { parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { return "", false } return parts[1], true } // Browsers cannot set custom Authorization headers for native WebSocket // handshakes, so allow `?token=` specifically for upgrade requests. if strings.EqualFold(c.GetHeader("Upgrade"), "websocket") { token := strings.TrimSpace(c.Query("token")) if token != "" { return token, true } } return "", false } func getJWTSecret() string { // Get from environment variable, fallback to default // Must match the secret used in config.go if secret := os.Getenv("JWT_SECRET"); secret != "" { return secret } return "clawreef-dev-secret-key-change-in-production" } func validateUserAccessToken(tokenString string) (*utils.TokenClaims, error) { jwtSecret := getJWTSecret() claims, err := utils.ValidateToken(tokenString, jwtSecret) if err != nil { return nil, err } if claims.TokenType != "access" { return nil, errors.New("invalid token type") } return claims, nil }