Skip to content

Commit

Permalink
Mercedes: generalize provider login (#2384)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jan 26, 2022
1 parent b031610 commit b4a004b
Show file tree
Hide file tree
Showing 19 changed files with 243 additions and 233 deletions.
21 changes: 3 additions & 18 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,25 +196,10 @@ type WebController interface {
WebControl(*mux.Router)
}

type Callback struct {
Path string
Handler RedirectHandlerFunc
}

// RedirectHandlerFunc should return an http.HandlerFunc responding with an http.Redirect(..., redirectURi, ...)
type RedirectHandlerFunc func(redirectURI string) http.HandlerFunc
// ProviderLogin is the ability to provide OAuth authentication through the ui
type ProviderLogin interface {
SetBasePath(basePath string)

// Provides ....
Callback() Callback
SetOAuthCallbackURI(uri string)

LoggedIn() bool

LoginPath() string
SetCallbackParams(uri string, authenticated chan<- bool)
LoginHandler() http.HandlerFunc

LogoutPath() string
LogoutHandler() http.HandlerFunc
CallbackHandler(baseURI string) http.HandlerFunc
}
3 changes: 0 additions & 3 deletions assets/js/authapi.js → assets/js/baseapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ const { protocol, hostname, port, pathname } = window.location;

const baseAPI = axios.create({
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname + "/",
headers: {
Accept: "application/json",
},
});

// global error handling
Expand Down
22 changes: 11 additions & 11 deletions assets/js/views/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import Notifications from "../components/Notifications";
import store from "../store";
import authAPI from "../authapi";
import baseAPI from "../baseapi";
export default {
name: "App",
Expand Down Expand Up @@ -155,14 +155,14 @@ export default {
return this.providerLogins.filter((login) => !login.loggedIn).length;
},
providerLogins() {
return this.store.state.loadpoints
.filter((lp) => lp.vehicleProviderLoginPath && lp.vehicleProviderLogoutPath)
.map((lp) => ({
title: lp.vehicleTitle,
loggedIn: lp.vehicleProviderLoggedIn,
loginPath: lp.vehicleProviderLoginPath,
logoutPath: lp.vehicleProviderLogoutPath,
}));
return this.store.state.auth
? Object.entries(this.store.state.auth.vehicles).map(([k, v]) => ({
title: k,
loggedIn: v.authenticated,
loginPath: v.uri + "/login",
logoutPath: v.uri + "/logout",
}))
: [];
},
},
created: function () {
Expand Down Expand Up @@ -212,11 +212,11 @@ export default {
},
handleProviderAuthorization: async function (provider) {
if (!provider.loggedIn) {
authAPI.post(provider.loginPath).then(function (response) {
baseAPI.post(provider.loginPath).then(function (response) {
window.location.href = response.data.loginUri;
});
} else {
authAPI.post(provider.logoutPath);
baseAPI.post(provider.logoutPath);
}
},
},
Expand Down
62 changes: 40 additions & 22 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
"github.com/evcc-io/evcc/provider/mqtt"
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle"
"github.com/evcc-io/evcc/vehicle/wrapper"
"github.com/gorilla/handlers"
)

type config struct {
Expand Down Expand Up @@ -79,6 +81,7 @@ type ConfigProvider struct {
chargers map[string]api.Charger
vehicles map[string]api.Vehicle
visited map[string]bool
auth *util.AuthCollection
}

func (cp *ConfigProvider) TrackVisitors() {
Expand Down Expand Up @@ -200,46 +203,61 @@ func (cp *ConfigProvider) configureVehicles(conf config) error {
return nil
}

// webControl handles implemented routes by devices.
// for now only api.ProviderLogin related routes
func (cp *ConfigProvider) webControl(httpd *server.HTTPd) {
func canonicalName(s string) string {
return strings.ToLower(strings.ReplaceAll(s, " ", "_"))
}

// webControl handles routing for devices. For now only api.ProviderLogin related routes
func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Param) {
router := httpd.Router()

auth := router.PathPrefix("/auth").Subrouter()
auth.Use(handlers.CompressHandler)
auth.Use(handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type"}),
))

// initialize
cp.auth = util.NewAuthCollection(paramC)

for _, v := range cp.vehicles {
if provider, ok := v.(api.ProviderLogin); ok {
title := url.QueryEscape(strings.ToLower(strings.ReplaceAll(v.Title(), " ", "_")))
title := url.QueryEscape(canonicalName(v.Title()))
basePath := fmt.Sprintf("vehicles/%s", title)

basePath := fmt.Sprintf("/auth/vehicles/%s", title)
provider.SetBasePath(basePath)
// TODO make evccURI configurable, add warnings for any network/ localhost
evccURI := fmt.Sprintf("http://%s", httpd.Addr)
baseURI := fmt.Sprintf("%s/auth/%s", evccURI, basePath)

callback := provider.Callback()
callbackURI := fmt.Sprintf("http://%s%s", httpd.Addr, callback.Path)
{
provider.SetOAuthCallbackURI(callbackURI)
log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), callbackURI)
}
// register vehicle
ap := cp.auth.Register(v.Title(), baseURI)

redirectURI := fmt.Sprintf("%s/callback", baseURI)
provider.SetCallbackParams(redirectURI, ap.Handler())
log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), redirectURI)

// TODO: how to handle multiple vehicles of the same type
// TODO how to handle multiple vehicles of the same type
//
// problems, thoughts and ideas:
// conflicting callbacks!
// - some unique part has to be added.
// - or a general callback handler and the specific vehicle is transported in the state?
// - callback handler needs an option to set the token at the right vehicle and use the right code exchange

// TODO: what about https?
router.
auth.
Methods(http.MethodGet).
Path(callback.Path).
HandlerFunc(callback.Handler(fmt.Sprintf("http://%s", httpd.Addr)))

router.
Path(fmt.Sprintf("/%s/callback", basePath)).
HandlerFunc(provider.CallbackHandler(evccURI))
auth.
Methods(http.MethodPost).
Path(provider.LoginPath()).
Path(fmt.Sprintf("/%s/login", basePath)).
HandlerFunc(provider.LoginHandler())
router.
auth.
Methods(http.MethodPost).
Path(provider.LogoutPath()).
Path(fmt.Sprintf("/%s/logout", basePath)).
HandlerFunc(provider.LogoutHandler())
}
}

cp.auth.Publish()
}
6 changes: 3 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,6 @@ func run(cmd *cobra.Command, args []string) {
socketHub := server.NewSocketHub()
httpd := server.NewHTTPd(uri, site, socketHub, cache)

// allow web access for vehicles
cp.webControl(httpd)

// metrics
if viper.GetBool("metrics") {
httpd.Router().Handle("/metrics", promhttp.Handler())
Expand Down Expand Up @@ -219,6 +216,9 @@ func run(cmd *cobra.Command, args []string) {
valueChan <- util.Param{Key: "sponsor", Val: sponsor.Subject}
}

// allow web access for vehicles
cp.webControl(httpd, valueChan)

// version check
go updater.Run(log, httpd, tee, valueChan)

Expand Down
16 changes: 0 additions & 16 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,9 +542,6 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.startVehicleDetection()
}

// publish providerLogins
lp.publishProviderLogins()

// read initial charger state to prevent immediately disabling charger
if enabled, err := lp.charger.Enabled(); err == nil {
if lp.enabled = enabled; enabled {
Expand Down Expand Up @@ -1404,9 +1401,6 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
// update progress and soc before status is updated
lp.publishChargeProgress()

// publish providerLogins
lp.publishProviderLogins()

// read and publish status
if err := lp.updateChargerStatus(); err != nil {
lp.log.ERROR.Printf("charger: %v", err)
Expand Down Expand Up @@ -1536,13 +1530,3 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
lp.log.ERROR.Println(err)
}
}

func (lp *LoadPoint) publishProviderLogins() {
for _, vehicle := range lp.vehicles {
if provider, ok := vehicle.(api.ProviderLogin); ok {
lp.publish("vehicleProviderLoggedIn", provider.LoggedIn())
lp.publish("vehicleProviderLoginPath", provider.LoginPath())
lp.publish("vehicleProviderLogoutPath", provider.LogoutPath())
}
}
}

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.55163eb0.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.b87bdfa4.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.55163eb0.css" rel="stylesheet"></head><body><script>window.evcc = {
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.6aeecebf.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.e5e2e523.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.6aeecebf.css" rel="stylesheet"></head><body><script>window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
commit: "[[.Commit]]",
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.b87bdfa4.js"></script></body></html>
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.e5e2e523.js"></script></body></html>
2 changes: 0 additions & 2 deletions dist/js/index.b87bdfa4.js

This file was deleted.

1 change: 0 additions & 1 deletion dist/js/index.b87bdfa4.js.map

This file was deleted.

2 changes: 2 additions & 0 deletions dist/js/index.e5e2e523.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/js/index.e5e2e523.js.map

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ func NewHTTPd(url string, site site.API, hub *SocketHub, cache *util.Cache) *HTT
api.Use(jsonHandler)
api.Use(handlers.CompressHandler)
api.Use(handlers.CORS(
handlers.AllowedHeaders([]string{
"Content-Type",
}),
handlers.AllowedHeaders([]string{"Content-Type"}),
))

// site api
Expand Down
64 changes: 64 additions & 0 deletions util/providerauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package util

import "sync"

type AuthCollection struct {
mu sync.Mutex
paramC chan<- Param
vehicles map[string]*AuthProvider
}

func NewAuthCollection(paramC chan<- Param) *AuthCollection {
return &AuthCollection{
paramC: paramC,
vehicles: make(map[string]*AuthProvider),
}
}

func (ac *AuthCollection) Register(title, baseURI string) *AuthProvider {
ap := &AuthProvider{
ac: ac,
Uri: baseURI,
}

ac.mu.Lock()
ac.vehicles[title] = ap
ac.mu.Unlock()

return ap
}

// publish routes and status
func (ac *AuthCollection) Publish() {
ac.mu.Lock()
defer ac.mu.Unlock()

val := struct {
Vehicles map[string]*AuthProvider `json:"vehicles"`
}{
Vehicles: ac.vehicles,
}

ac.paramC <- Param{Key: "auth", Val: val}
}

type AuthProvider struct {
ac *AuthCollection
Uri string `json:"uri"`
Authenticated bool `json:"authenticated"`
}

func (ap *AuthProvider) Handler() chan<- bool {
c := make(chan bool)

go func() {
for auth := range c {
ap.ac.mu.Lock()
ap.Authenticated = auth
ap.ac.mu.Unlock()
ap.ac.Publish()
}
}()

return c
}
10 changes: 4 additions & 6 deletions vehicle/mercedes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {

var options []mercedes.IdentityOptions

// TODO: Load tokens from a persistence storage and use those during startup
// TODO Load tokens from a persistence storage and use those during startup
// e.g. persistence.Load("key")
// if tokens != nil {
// options = append(options, mercedes.WithToken(&oauth2.Token{
Expand All @@ -54,15 +54,13 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {

log := util.NewLogger("mercedes")

updateC := make(chan struct{})

// TODO: session secret from config/persistence
identity, err := mercedes.NewIdentity(log, cc.ClientID, cc.ClientSecret, updateC, options...)
// TODO session secret from config/persistence
identity, err := mercedes.NewIdentity(log, cc.ClientID, cc.ClientSecret, options...)
if err != nil {
return nil, err
}

api := mercedes.NewAPI(log, identity, updateC)
api := mercedes.NewAPI(log, identity)

v := &Mercedes{
embed: &cc.embed,
Expand Down
Loading

0 comments on commit b4a004b

Please sign in to comment.