package middleware import ( "context" "net/http" "strings" "time" "music-server/internal/db/repository" "music-server/internal/logging" "github.com/danielgtaylor/huma/v2" "go.uber.org/zap" ) type tokenKey struct{} // Token returns the session token of an authenticated request. func Token(ctx context.Context) string { token, _ := ctx.Value(tokenKey{}).(string) return token } // BearerToken extracts the token from an "Authorization: Bearer " header. func BearerToken(header string) (string, bool) { token, ok := strings.CutPrefix(header, "Bearer ") return token, ok && token != "" && !strings.Contains(token, " ") } // TokenAuth validates the session token for operations that declare a security requirement. // When disabled is true all requests are let through. func TokenAuth(api huma.API, q repository.Querier, disabled bool) Middleware { return func(ctx huma.Context, next func(huma.Context)) { if disabled || len(ctx.Operation().Security) == 0 { next(ctx) return } header := ctx.Header("Authorization") if header == "" { _ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Authorization header required") return } token, ok := BearerToken(header) if !ok { _ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Invalid authorization format. Use: Bearer ") return } session, err := q.GetSession(ctx.Context(), token) if err != nil { logging.GetLogger().Warn("Invalid token attempt", zap.String("ip", clientIP(ctx)), zap.Error(err)) _ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Invalid or expired token") return } 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.Context())) _ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Token expired") return } next(huma.WithValue(ctx, tokenKey{}, token)) } } type clientKey struct{} // Client holds request details that handlers need but that are not part of the API input. type Client struct { IP string UserAgent string } // ClientInfo stores the client's IP and user agent in the request context. func ClientInfo(ctx huma.Context, next func(huma.Context)) { next(huma.WithValue(ctx, clientKey{}, Client{IP: clientIP(ctx), UserAgent: ctx.Header("User-Agent")})) } // ClientFrom returns the client details stored by ClientInfo. func ClientFrom(ctx context.Context) Client { client, _ := ctx.Value(clientKey{}).(Client) return client }