Merge commit from fork

* Config: require admin password for script plugins

* refactor
This commit is contained in:
Michael Geers 2026-06-24 15:45:15 +02:00 • committed by GitHub
parent a193236395
commit 4c907c5afa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 594 additions and 207 deletions

View file

@ -21,6 +21,7 @@ import (
"github.com/evcc-io/evcc/meter"
"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/tariff"
"github.com/evcc-io/evcc/util/auth"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/templates"
"github.com/evcc-io/evcc/vehicle"
@ -308,66 +309,72 @@ func newDevice[T any](ctx context.Context, class templates.Class, req configReq,
}
// newDeviceHandler creates a new device by class
func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
func newDeviceHandler(authObject auth.Auth) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if !requireCriticalConfigAuth(w, r, authObject, req) {
return
}
var conf *config.Config
ctx, cancel, done := startDeviceTimeout()
force := r.URL.Query().Get("force") == "true"
switch class {
case templates.Charger:
conf, err = newDevice(ctx, class, req, charger.NewFromConfig, config.Chargers(), force)
case templates.Meter:
conf, err = newDevice(ctx, class, req, meter.NewFromConfig, config.Meters(), force)
case templates.Vehicle:
conf, err = newDevice(ctx, class, req, vehicle.NewFromConfig, config.Vehicles(), force)
case templates.Circuit:
conf, err = newDevice(ctx, class, req, circuit.NewFromConfig, config.Circuits(), force)
case templates.Tariff:
conf, err = newDevice(ctx, class, req, tariff.NewFromConfig, config.Tariffs(), force)
case templates.Messenger:
conf, err = newDevice(ctx, class, req, messenger.NewFromConfig, config.Messengers(), force)
}
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled
close(done)
setConfigDirty()
res := struct {
ID int `json:"id"`
Name string `json:"name"`
}{
ID: conf.ID,
Name: config.NameForID(conf.ID),
}
jsonWrite(w, res)
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
var conf *config.Config
ctx, cancel, done := startDeviceTimeout()
force := r.URL.Query().Get("force") == "true"
switch class {
case templates.Charger:
conf, err = newDevice(ctx, class, req, charger.NewFromConfig, config.Chargers(), force)
case templates.Meter:
conf, err = newDevice(ctx, class, req, meter.NewFromConfig, config.Meters(), force)
case templates.Vehicle:
conf, err = newDevice(ctx, class, req, vehicle.NewFromConfig, config.Vehicles(), force)
case templates.Circuit:
conf, err = newDevice(ctx, class, req, circuit.NewFromConfig, config.Circuits(), force)
case templates.Tariff:
conf, err = newDevice(ctx, class, req, tariff.NewFromConfig, config.Tariffs(), force)
case templates.Messenger:
conf, err = newDevice(ctx, class, req, messenger.NewFromConfig, config.Messengers(), force)
}
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled
close(done)
setConfigDirty()
res := struct {
ID int `json:"id"`
Name string `json:"name"`
}{
ID: conf.ID,
Name: config.NameForID(conf.ID),
}
jsonWrite(w, res)
}
func updateDevice[T any](ctx context.Context, id int, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T], force bool) error {
@ -388,69 +395,75 @@ func updateDevice[T any](ctx context.Context, id int, class templates.Class, req
}
// updateDeviceHandler updates database device's configuration by class
func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
func updateDeviceHandler(authObject auth.Auth) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
id, err := strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if !requireCriticalConfigAuth(w, r, authObject, req) {
return
}
ctx, cancel, done := startDeviceTimeout()
force := r.URL.Query().Get("force") == "true"
switch class {
case templates.Charger:
err = updateDevice(ctx, id, class, req, charger.NewFromConfig, config.Chargers(), force)
case templates.Meter:
err = updateDevice(ctx, id, class, req, meter.NewFromConfig, config.Meters(), force)
case templates.Vehicle:
err = updateDevice(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles(), force)
case templates.Circuit:
err = updateDevice(ctx, id, class, req, circuit.NewFromConfig, config.Circuits(), force)
case templates.Tariff:
err = updateDevice(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs(), force)
case templates.Messenger:
err = updateDevice(ctx, id, class, req, messenger.NewFromConfig, config.Messengers(), force)
}
setConfigDirty()
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled
close(done)
res := struct {
ID int `json:"id"`
}{
ID: id,
}
jsonWrite(w, res)
}
id, err := strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
ctx, cancel, done := startDeviceTimeout()
force := r.URL.Query().Get("force") == "true"
switch class {
case templates.Charger:
err = updateDevice(ctx, id, class, req, charger.NewFromConfig, config.Chargers(), force)
case templates.Meter:
err = updateDevice(ctx, id, class, req, meter.NewFromConfig, config.Meters(), force)
case templates.Vehicle:
err = updateDevice(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles(), force)
case templates.Circuit:
err = updateDevice(ctx, id, class, req, circuit.NewFromConfig, config.Circuits(), force)
case templates.Tariff:
err = updateDevice(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs(), force)
case templates.Messenger:
err = updateDevice(ctx, id, class, req, messenger.NewFromConfig, config.Messengers(), force)
}
setConfigDirty()
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled
close(done)
res := struct {
ID int `json:"id"`
}{
ID: id,
}
jsonWrite(w, res)
}
func configurableDevice[T any](name string, h config.Handler[T]) (config.ConfigurableDevice[T], error) {
@ -644,67 +657,73 @@ func testConfig[T any](ctx context.Context, id int, class templates.Class, req c
}
// testConfigHandler tests a configuration by class
func testConfigHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
func testConfigHandler(authObject auth.Auth) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
var id int
if vars["id"] != "" {
// test existing device with updated config
id, err = strconv.Atoi(vars["id"])
class, err := templates.ClassString(vars["class"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
var id int
if vars["id"] != "" {
// test existing device with updated config
id, err = strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if !requireCriticalConfigAuth(w, r, authObject, req) {
return
}
var instance any
ctx, cancel, done := startDeviceTimeout()
switch class {
case templates.Charger:
instance, err = testConfig(ctx, id, class, req, charger.NewFromConfig, config.Chargers())
case templates.Meter:
instance, err = testConfig(ctx, id, class, req, meter.NewFromConfig, config.Meters())
case templates.Vehicle:
instance, err = testConfig(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles())
case templates.Circuit:
instance, err = testConfig(ctx, id, class, req, circuit.NewFromConfig, config.Circuits())
case templates.Tariff:
instance, err = testConfig(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs())
case templates.Messenger:
instance, err = testConfig(ctx, id, class, req, messenger.NewFromConfig, config.Messengers())
}
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled during test
close(done)
defer cancel()
// bound the value-probe phase so a blocking getter cannot stall the response
probeCtx, probeCancel := context.WithTimeout(r.Context(), 10*time.Second)
defer probeCancel()
jsonWrite(w, testInstance(probeCtx, instance))
}
req, err := decodeDeviceConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
var instance any
ctx, cancel, done := startDeviceTimeout()
switch class {
case templates.Charger:
instance, err = testConfig(ctx, id, class, req, charger.NewFromConfig, config.Chargers())
case templates.Meter:
instance, err = testConfig(ctx, id, class, req, meter.NewFromConfig, config.Meters())
case templates.Vehicle:
instance, err = testConfig(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles())
case templates.Circuit:
instance, err = testConfig(ctx, id, class, req, circuit.NewFromConfig, config.Circuits())
case templates.Tariff:
instance, err = testConfig(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs())
case templates.Messenger:
instance, err = testConfig(ctx, id, class, req, messenger.NewFromConfig, config.Messengers())
}
if err != nil {
cancel()
jsonError(w, http.StatusBadRequest, err)
return
}
// prevent context from being cancelled during test
close(done)
defer cancel()
// bound the value-probe phase so a blocking getter cannot stall the response
probeCtx, probeCancel := context.WithTimeout(r.Context(), 10*time.Second)
defer probeCancel()
jsonWrite(w, testInstance(probeCtx, instance))
}