From 1f0bd92518c9ff0f449ffb35e3348c9537cce86d Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 20 Nov 2025 19:02:18 +0100 Subject: [PATCH] Config UI: add provider auth subject to login errors (#25342) --- api/error.go | 19 ++++++++++++++++--- plugin/auth/oauth.go | 2 +- plugin/auth/oauth_test.go | 2 +- server/http_site_handler.go | 28 ++-------------------------- util/error.go | 36 ++++++++++++++++++++++++++++++++++++ vehicle/bmw/cardata/token.go | 3 ++- 6 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 util/error.go diff --git a/api/error.go b/api/error.go index 64eafc96f..a55541d33 100644 --- a/api/error.go +++ b/api/error.go @@ -18,9 +18,6 @@ var ErrSponsorRequired = errors.New("sponsorship required, see https://docs.evcc // ErrMissingCredentials indicates that user/password are missing var ErrMissingCredentials = backoff.Permanent(errors.New("missing user/password credentials")) -// ErrLoginRequired indicates that retrieving tokens credentials waits for login -var ErrLoginRequired = backoff.Permanent(errors.New("login required")) - // ErrMissingToken indicates that access/refresh tokens are missing var ErrMissingToken = backoff.Permanent(errors.New("missing token credentials")) @@ -30,6 +27,22 @@ var ErrOutdated = errors.New("outdated") // ErrTimeout is the error returned when a timeout happened var ErrTimeout error = errors.New("timeout") +// LoginRequiredError creates a login error for given auth provider +func LoginRequiredError(providerAuth string) error { + return backoff.Permanent(&ErrLoginRequired{ + ProviderAuth: providerAuth, + }) +} + +// ErrLoginRequired indicates that retrieving tokens credentials waits for login +type ErrLoginRequired struct { + ProviderAuth string +} + +func (err *ErrLoginRequired) Error() string { + return "login required" +} + // ErrAsleep indicates that vehicle is asleep. Caller may chose to wake up the vehicle and retry. var ErrAsleep error = errAsleep{} diff --git a/plugin/auth/oauth.go b/plugin/auth/oauth.go index b7bad8152..2bba2d26e 100644 --- a/plugin/auth/oauth.go +++ b/plugin/auth/oauth.go @@ -160,7 +160,7 @@ func (o *OAuth) Token() (*oauth2.Token, error) { defer o.mu.Unlock() if o.token == nil { - return nil, api.ErrMissingToken + return nil, api.LoginRequiredError(o.subject) } if o.token.Valid() { diff --git a/plugin/auth/oauth_test.go b/plugin/auth/oauth_test.go index 1660fa708..2881ad103 100644 --- a/plugin/auth/oauth_test.go +++ b/plugin/auth/oauth_test.go @@ -21,7 +21,7 @@ func TestOAuth(t *testing.T) { require.NoError(t, err) token, err := ts.Token() - require.ErrorContains(t, err, "missing token credentials") + require.ErrorContains(t, err, "login required") require.False(t, token.Valid()) require.Equal(t, 0, storerCalled) diff --git a/server/http_site_handler.go b/server/http_site_handler.go index f79cb1e38..eca99afa1 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -26,7 +26,6 @@ import ( "github.com/evcc-io/evcc/util/logstash" "github.com/gorilla/mux" "github.com/itchyny/gojq" - "go.yaml.in/yaml/v4" "golang.org/x/text/language" ) @@ -82,35 +81,12 @@ func jsonHandler(h http.Handler) http.Handler { } func jsonWrite(w http.ResponseWriter, data any) { - if err := json.NewEncoder(w).Encode(data); err != nil { - log.ERROR.Printf("httpd: failed to encode JSON: %v", err) - } + json.NewEncoder(w).Encode(data) } func jsonError(w http.ResponseWriter, status int, err error) { w.WriteHeader(status) - - res := struct { - Error string `json:"error"` - Line int `json:"line,omitempty"` - IsAuthError bool `json:"isAuthError,omitempty"` - }{ - Error: err.Error(), - IsAuthError: errors.Is(err, api.ErrLoginRequired) || errors.Is(err, api.ErrMissingToken), - } - - var ( - ype *yaml.ParserError - yue *yaml.UnmarshalError - ) - switch { - case errors.As(err, &ype): - res.Line = ype.Line - case errors.As(err, &yue): - res.Line = yue.Line - } - - jsonWrite(w, res) + jsonWrite(w, util.ErrorAsJson(err)) } func handler[T any](conv func(string) (T, error), set func(T) error, get func() T) http.HandlerFunc { diff --git a/util/error.go b/util/error.go new file mode 100644 index 000000000..178e1b4a4 --- /dev/null +++ b/util/error.go @@ -0,0 +1,36 @@ +package util + +import ( + "errors" + + "github.com/evcc-io/evcc/api" + "go.yaml.in/yaml/v4" +) + +// ErrorAsJson returns an error as json-formattable struct +func ErrorAsJson(err error) any { + res := struct { + Error string `json:"error"` + Line int `json:"line,omitempty"` + LoginRequired string `json:"loginRequired,omitempty"` + }{ + Error: err.Error(), + } + + if ae := new(api.ErrLoginRequired); errors.As(err, &ae) { + res.LoginRequired = ae.ProviderAuth + } + + var ( + ype *yaml.ParserError + yue *yaml.UnmarshalError + ) + switch { + case errors.As(err, &ype): + res.Line = ype.Line + case errors.As(err, &yue): + res.Line = yue.Line + } + + return res +} diff --git a/vehicle/bmw/cardata/token.go b/vehicle/bmw/cardata/token.go index 6a8e7a3b0..0cec59c59 100644 --- a/vehicle/bmw/cardata/token.go +++ b/vehicle/bmw/cardata/token.go @@ -29,5 +29,6 @@ func TokenExtra(t *oauth2.Token, key string) string { } func tokenError(err error) bool { - return errors.Is(err, api.ErrLoginRequired) || errors.Is(err, api.ErrMissingToken) + ae := new(api.ErrLoginRequired) + return errors.As(err, &ae) }