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 echo.NewHTTPError(http.StatusUnauthorized, "Authorization header required") } token, ok := BearerToken(c) if !ok { return echo.NewHTTPError(http.StatusUnauthorized, "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 echo.NewHTTPError(http.StatusUnauthorized, "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 echo.NewHTTPError(http.StatusUnauthorized, "Token expired") } c.Set("session", session) return next(c) } } }