evcc-io/util/sponsor/auth.go
Michael Geers 4163748e50
Config UI: improve sponsor token flow (#22989)
Co-authored-by: andig <andi@evcc.io>
Co-authored-by: andig <cpuidle@gmail.com>
2025-08-15 10:02:14 +00:00

126 lines
2.5 KiB
Go

package sponsor
import (
"context"
"fmt"
"sync"
"time"
"github.com/evcc-io/evcc/api/proto/pb"
"github.com/evcc-io/evcc/util/cloud"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
mu sync.RWMutex
Subject, Token string
fromYaml bool = true
ExpiresAt time.Time
)
const (
unavailable = "sponsorship unavailable"
victron = "victron"
)
func IsAuthorized() bool {
mu.RLock()
defer mu.RUnlock()
return len(Subject) > 0
}
func IsAuthorizedForApi() bool {
mu.RLock()
defer mu.RUnlock()
return IsAuthorized() && Subject != unavailable && Token != ""
}
// SetFromYaml sets whether the token comes from YAML config or database
func SetFromYaml(val bool) {
mu.Lock()
defer mu.Unlock()
fromYaml = val
}
// check and set sponsorship token
func ConfigureSponsorship(token string) error {
mu.Lock()
defer mu.Unlock()
if token == "" {
if sub := checkVictron(); sub != "" {
Subject = sub
return nil
}
var err error
if token, err = readSerial(); token == "" || err != nil {
return err
}
}
Token = token
conn, err := cloud.Connection()
if err != nil {
return err
}
client := pb.NewAuthClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := client.IsAuthorized(ctx, &pb.AuthRequest{Token: token})
if err == nil && res.Authorized {
Subject = res.Subject
ExpiresAt = res.ExpiresAt.AsTime()
}
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() != codes.Unknown {
Subject = unavailable
err = nil
} else {
err = fmt.Errorf("sponsortoken: %w", err)
}
}
return err
}
// redactToken returns a redacted version of the token showing only start and end characters
func redactToken(token string) string {
if len(token) <= 12 {
return ""
}
return token[:6] + "......." + token[len(token)-6:]
}
type sponsorStatus struct {
Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
ExpiresSoon bool `json:"expiresSoon,omitempty"`
Token string `json:"token,omitempty"`
FromYaml bool `json:"fromYaml"`
}
// Status returns the sponsorship status
func Status() sponsorStatus {
mu.RLock()
defer mu.RUnlock()
var expiresSoon bool
if d := time.Until(ExpiresAt); d < 30*24*time.Hour && d > 0 {
expiresSoon = true
}
return sponsorStatus{
Name: Subject,
ExpiresAt: ExpiresAt,
ExpiresSoon: expiresSoon,
Token: redactToken(Token),
FromYaml: fromYaml,
}
}