package middlewares import ( "strings" "ares/services" "github.com/gofiber/fiber/v3" ) const authClaimsKey = "auth_claims" func RequireAuth(c fiber.Ctx) error { // First try Authorization header (Bearer JWT) authHeader := strings.TrimSpace(c.Get("Authorization")) if authHeader != "" { parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) == "" { // If request originates from browser/HTMX, redirect to login instead of returning JSON if c.Get("HX-Request") == "true" { c.Set("HX-Redirect", "/login") return c.SendStatus(fiber.StatusOK) } accept := strings.ToLower(c.Get("Accept")) if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") { return c.Redirect().To("/login") } return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization format, expected: Bearer "}) } jwtService := services.NewJWTService() claims, err := jwtService.ValidateToken(strings.TrimSpace(parts[1])) if err != nil { if c.Get("HX-Request") == "true" { c.Set("HX-Redirect", "/login") return c.SendStatus(fiber.StatusOK) } accept := strings.ToLower(c.Get("Accept")) if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") { return c.Redirect().To("/login") } return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"}) } if claims.TokenType != services.TokenTypeAccess { if c.Get("HX-Request") == "true" { c.Set("HX-Redirect", "/login") return c.SendStatus(fiber.StatusOK) } accept := strings.ToLower(c.Get("Accept")) if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") { return c.Redirect().To("/login") } return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "access token required"}) } c.Locals(authClaimsKey, claims) return c.Next() } // Fallback: check cookie-based admin session (browser login) — expect signed JWT cookie := c.Cookies("admin_session") if cookie != "" { jwtService := services.NewJWTService() if claims, err := jwtService.ValidateToken(cookie); err == nil { if claims.TokenType == services.TokenTypeAccess { c.Locals(authClaimsKey, claims) return c.Next() } } } // Default unauthorized response: redirect to login for browser requests, JSON for API clients if c.Get("HX-Request") == "true" { c.Set("HX-Redirect", "/login") return c.SendStatus(fiber.StatusOK) } accept := strings.ToLower(c.Get("Accept")) if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") { return c.Redirect().To("/login") } return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "authorization header is required"}) } func RequireAdmin(c fiber.Ctx) error { claims, ok := GetAuthClaims(c) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"}) } if !claims.IsAdmin { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "admin role required"}) } return c.Next() } func RequireNormalUser(c fiber.Ctx) error { claims, ok := GetAuthClaims(c) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"}) } if claims.IsAdmin { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "only normal users can access this endpoint"}) } return c.Next() } func GetAuthClaims(c fiber.Ctx) (*services.JWTClaim, bool) { raw := c.Locals(authClaimsKey) claims, ok := raw.(*services.JWTClaim) return claims, ok }