package middleware import ( "context" "net/http" "strings" "time" "music-server/internal/db/repository" "music-server/internal/logging" "github.com/labstack/echo/v5" "go.uber.org/zap" ) // BearerToken extracts the token from an "Authorization: Bearer " header. func BearerToken(c *echo.Context) (string, bool) { token, ok := strings.CutPrefix(c.Request().Header.Get("Authorization"), "Bearer ") return token, ok && token != "" && !strings.Contains(token, " ") } // TokenAuth validates session tokens. When disabled is true all requests are let through. func TokenAuth(q repository.Querier, disabled bool) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if disabled { return next(c) } if c.Request().Header.Get("Authorization") == "" { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authorization header required"}) } token, ok := BearerToken(c) if !ok { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid authorization format. Use: Bearer "}) } ctx := c.Request().Context() session, err := q.GetSession(ctx, token) if err != nil { logging.GetLogger().Warn("Invalid token attempt", zap.String("ip", c.RealIP()), zap.String("error", err.Error()), ) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid or expired token"}) } if time.Now().After(session.ExpiresAt.Time) { // Clean up in the background with a context that outlives the request. go func(ctx context.Context) { if err := q.DeleteSession(ctx, token); err != nil { logging.GetLogger().Error("Failed to delete expired session", zap.Error(err)) } }(context.WithoutCancel(ctx)) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Token expired"}) } c.Set("session", session) return next(c) } } }