Issue UI: redact private data like (user, locations, ...) (#25039)

This commit is contained in:
Copilot 2025-11-26 13:34:57 +01:00 • committed by GitHub
parent f8dcd4dd78
commit a65a1ce2d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 277 additions and 109 deletions

View file

@ -475,7 +475,8 @@ export default defineComponent({
for (const endpoint of endpoints) {
try {
const response = await api.get(endpoint);
// Add private=false for device endpoints to hide private data in bug reports
const response = await api.get(endpoint, { params: { private: false } });
if (response.data && Object.keys(response.data).length > 0) {
const key = endpoint.replace("config/", "").replace("devices/", "");
let data = response.data;

View file

@ -5,6 +5,7 @@ import (
"strings"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/redact"
"github.com/evcc-io/evcc/util/templates"
"github.com/spf13/cobra"
)
@ -54,7 +55,7 @@ func runConfig(cmd *cobra.Command, args []string) {
}
for _, c := range configurable {
fmt.Println(config.NameForID(c.ID), fmt.Sprintf("%+v", c.Properties), redactMap(c.Data))
fmt.Println(config.NameForID(c.ID), fmt.Sprintf("%+v", c.Properties), redact.Map(c.Data))
}
fmt.Println("")

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/browser"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/redact"
"github.com/spf13/cobra"
)
@ -44,7 +45,7 @@ func runDiscuss(cmd *cobra.Command, args []string) {
var redacted string
if src, err := os.ReadFile(cfgFile); err == nil {
redacted = redact(string(src))
redacted = redact.String(string(src))
}
tmpl := template.Must(template.New("discuss").Parse(discussTmpl))

View file

@ -13,6 +13,7 @@ import (
"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/redact"
"github.com/spf13/cobra"
)
@ -67,7 +68,7 @@ func runDump(cmd *cobra.Command, args []string) {
var redacted string
if src, err := os.ReadFile(cfgFile); err == nil {
redacted = redact(string(src))
redacted = redact.String(string(src))
}
tmpl := template.Must(

View file

@ -49,14 +49,6 @@ func unwrap(err error) (res []string) {
return
}
func redact(src string) string {
return util.RedactConfigString(src)
}
func redactMap(src map[string]any) map[string]any {
return util.RedactConfigMap(src)
}
// fatal logs a fatal error and runs shutdown functions before terminating
func fatal(err error) {
log.FATAL.Println(err)

View file

@ -6,6 +6,8 @@ import (
"reflect"
"strings"
"testing"
"github.com/evcc-io/evcc/util/redact"
)
func TestUnwrap(t *testing.T) {
@ -23,17 +25,17 @@ func TestRedact(t *testing.T) {
sponsortoken: geheim
user: geheim
password: geheim
secret: geheim
clientsecret: geheim
token:
access: geheim
refresh: geheim
accesstoken: geheim
refreshtoken: geheim
pin: geheim
mac: geheim
secret: geheim # comment
secret : geheim
clientsecret: geheim # comment
clientsecret : geheim
`
if res := redact(secret); strings.Contains(res, "geheim") || !strings.Contains(res, "public") {
if res := redact.String(secret); strings.Contains(res, "geheim") || !strings.Contains(res, "public") {
t.Errorf("secret exposed: %v", res)
}
}

View file

@ -15,6 +15,7 @@ export default defineConfig({
video: "on-first-retry",
screenshot: "only-on-failure",
permissions: ["clipboard-write"],
actionTimeout: 20000, // 20s for individual actions
},
projects: [
{

View file

@ -23,11 +23,11 @@ import (
"go.yaml.in/yaml/v4"
)
func devicesConfig[T any](class templates.Class, h config.Handler[T]) ([]map[string]any, error) {
func devicesConfig[T any](class templates.Class, h config.Handler[T], hidePrivate bool) ([]map[string]any, error) {
var res []map[string]any
for _, dev := range h.Devices() {
dc, err := deviceConfigMap(class, dev)
dc, err := deviceConfigMap(class, dev, hidePrivate)
if err != nil {
return nil, err
}
@ -54,20 +54,23 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Check if private data should be hidden (default: true, showing private data)
hidePrivate := r.URL.Query().Get("private") == "false"
var res []map[string]any
switch class {
case templates.Meter:
res, err = devicesConfig(class, config.Meters())
res, err = devicesConfig(class, config.Meters(), hidePrivate)
case templates.Charger:
res, err = devicesConfig(class, config.Chargers())
res, err = devicesConfig(class, config.Chargers(), hidePrivate)
case templates.Vehicle:
res, err = devicesConfig(class, config.Vehicles())
res, err = devicesConfig(class, config.Vehicles(), hidePrivate)
case templates.Circuit:
res, err = devicesConfig(class, config.Circuits())
res, err = devicesConfig(class, config.Circuits(), hidePrivate)
}
if err != nil {
@ -78,7 +81,7 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
jsonWrite(w, res)
}
func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[string]any, error) {
func deviceConfigMap[T any](class templates.Class, dev config.Device[T], hidePrivate bool) (map[string]any, error) {
conf := dev.Config()
dc := map[string]any{
@ -102,7 +105,7 @@ func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[st
}
if conf.Type == typeTemplate {
params, err := sanitizeMasked(class, conf.Other)
params, err := sanitizeMasked(class, conf.Other, hidePrivate)
if err != nil {
return nil, err
}
@ -146,13 +149,13 @@ func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[st
return dc, nil
}
func deviceConfig[T any](class templates.Class, id int, h config.Handler[T]) (map[string]any, error) {
func deviceConfig[T any](class templates.Class, id int, h config.Handler[T], hidePrivate bool) (map[string]any, error) {
dev, err := h.ByName(config.NameForID(id))
if err != nil {
return nil, err
}
return deviceConfigMap(class, dev)
return deviceConfigMap(class, dev, hidePrivate)
}
// deviceConfigHandler returns a device configuration by class
@ -171,20 +174,23 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Check if private data should be hidden (default: true, showing private data)
hidePrivate := r.URL.Query().Get("private") == "false"
var res map[string]any
switch class {
case templates.Meter:
res, err = deviceConfig(class, id, config.Meters())
res, err = deviceConfig(class, id, config.Meters(), hidePrivate)
case templates.Charger:
res, err = deviceConfig(class, id, config.Chargers())
res, err = deviceConfig(class, id, config.Chargers(), hidePrivate)
case templates.Vehicle:
res, err = deviceConfig(class, id, config.Vehicles())
res, err = deviceConfig(class, id, config.Vehicles(), hidePrivate)
case templates.Circuit:
res, err = deviceConfig(class, id, config.Circuits())
res, err = deviceConfig(class, id, config.Circuits(), hidePrivate)
}
if err != nil {

View file

@ -133,7 +133,7 @@ func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) ma
return res
}
func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any, error) {
func sanitizeMasked(class templates.Class, conf map[string]any, hidePrivate bool) (map[string]any, error) {
tmpl, err := templateForConfig(class, conf)
if err != nil {
return nil, err
@ -142,8 +142,12 @@ func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any,
res := make(map[string]any, len(conf))
for k, v := range conf {
if i, p := tmpl.ParamByName(k); i >= 0 && p.IsMasked() {
if i, p := tmpl.ParamByName(k); i >= 0 {
if p.IsMasked() {
v = masked
} else if hidePrivate && p.IsPrivate() {
v = masked
}
}
res[k] = v

View file

@ -5,7 +5,7 @@ import (
"net/http"
"os"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/redact"
)
// configYamlHandler returns the redacted evcc.yaml configuration file
@ -25,7 +25,7 @@ func configYamlHandler(configFilePath string) http.HandlerFunc {
}
// Redact sensitive information
redacted := util.RedactConfigString(string(src))
redacted := redact.String(string(src))
// Return the redacted content as plain text
w.Header().Set("Content-Type", "text/plain; charset=utf-8")

View file

@ -11,6 +11,7 @@ import (
"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/redact"
"github.com/gorilla/mux"
"go.yaml.in/yaml/v4"
)
@ -18,6 +19,12 @@ import (
func settingsGetStringHandler(key string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, _ := settings.String(key)
// Check if private data should be hidden
if r.URL.Query().Get("private") == "false" && res != "" {
res = redact.String(res)
}
jsonWrite(w, res)
}
}

View file

@ -13,7 +13,6 @@ params:
required: true
- name: pin
required: true
mask: true
render: |
type: nrgkick-bluetooth
mac: {{ .mac }}

View file

@ -9,7 +9,6 @@ params:
- name: usage
choice: ["grid"]
- name: token
mask: true
required: true
example: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE
- name: homeid

View file

@ -7,7 +7,6 @@ params:
- name: vin
example: ZFAE...
- name: pin
mask: true
help:
en: Required for evcc to wake up the vehicle for charging and to refresh the SoC while charging. When connected to TWC3, used to start/stop charging.
de: Benötigt um das Fahrzeug zum Laden aufzuwecken and zur Aktualisierung des Ladestands während der Ladung. Bei Nutzung mit TWC3 kann der Ladevorgang mittels PIN gestartet und gestoppt werden.

View file

@ -6,24 +6,21 @@ products:
en: Auth Demo Meter
auth:
type: demo
params: ["region", "token"]
params: ["region", "server"]
params:
- name: usage
choice: ["grid"]
- name: region
description:
de: Server
en: Server
generic: Region
type: choice
choice: ["EU", "US", "CN"]
- name: token
- name: server
description:
de: Token
en: Token
generic: Server
- name: power
description:
de: Leistung
en: Power
generic: Power
unit: W
type: int

View file

@ -35,13 +35,13 @@ test.describe("config device auth", async () => {
await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
// step 1: auth view
await expect(meterModal.getByLabel("Region")).toBeVisible();
await expect(meterModal.getByLabel("Server")).toBeVisible();
await expect(meterModal.getByLabel("Token")).toBeVisible();
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
await expect(meterModal.getByRole("button", { name: "Validate & save" })).not.toBeVisible();
await expect(meterModal.getByRole("button", { name: "Save" })).not.toBeVisible();
await meterModal.getByLabel("Server").selectOption("EU");
await meterModal.getByLabel("Token").fill("test-token-123");
await meterModal.getByLabel("Region").selectOption("EU");
await meterModal.getByLabel("Server").fill("my.server.org");
await meterModal.getByRole("button", { name: "Prepare connection" }).click();
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();
@ -51,8 +51,8 @@ test.describe("config device auth", async () => {
});
// step 2: show regular device form
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
await expect(meterModal.getByLabel("Power")).toBeVisible();
await meterModal.getByLabel("Power").fill("5000");
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
@ -69,8 +69,8 @@ test.describe("config device auth", async () => {
// re-open meter for editing
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
await expect(meterModal.getByLabel("Power")).toHaveValue("5000");
await expect(meterModal.getByRole("button", { name: "Prepare connection" })).not.toBeVisible();
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
@ -84,8 +84,8 @@ test.describe("config device auth", async () => {
// re-open meter for editing after restart, auth status as to be reestablished
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
// note: prepare connection step is auto-executed, since all required fields (server, token) are already present
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();

View file

@ -1,9 +1,18 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await startSimulator();
});
test.afterAll(async () => {
await stopSimulator();
});
test.afterEach(async () => {
await stop();
});
@ -47,16 +56,18 @@ test.describe("issue creation", () => {
// Enable experimental features
await enableExperimental(page, false);
// Create a battery meter
await page.getByRole("button", { name: "Add solar or battery" }).click();
await page.getByRole("button", { name: "Add battery meter" }).click();
// Create a Shelly meter with username (to test private data redaction)
await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await expectModalVisible(meterModal);
await meterModal.getByLabel("Title").fill("BigBlueBattery");
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
await meterModal.getByLabel("Manufacturer").selectOption("Shelly 1PM");
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
await meterModal.getByLabel("Username").fill("testuser@example.com");
await meterModal.getByLabel("Password").fill("secretpass");
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("battery")).toBeVisible();
await expect(page.getByTestId("grid")).toBeVisible();
// Restart to apply changes
await restart(CONFIG);
@ -77,7 +88,7 @@ test.describe("issue creation", () => {
.fill("This is a test issue created from the config page workflow");
await page
.getByLabel("Steps to reproduce")
.fill("1. Go to config\n2. Enable experimental\n3. Add battery\n4. Report issue");
.fill("1. Go to config\n2. Enable experimental\n3. Add meter\n4. Report issue");
// check yaml data
const yamlItem = page.getByTestId("issueYamlConfig-additional-item");
@ -88,12 +99,19 @@ test.describe("issue creation", () => {
await yamlModal.getByRole("button", { name: "Close" }).first().click();
await expectModalHidden(yamlModal);
// check ui data
// check ui data and verify private data redaction
const uiItem = page.getByTestId("issueUiConfig-additional-item");
await uiItem.getByRole("button", { name: "show details" }).click();
const uiModal = page.getByTestId("issueUiConfig-modal");
await expectModalVisible(uiModal);
await expect(uiModal.getByRole("textbox")).toHaveValue(/BigBlueBattery/);
const uiContent = await uiModal.getByRole("textbox").inputValue();
// Verify meter is present but private data is redacted
expect(uiContent).toContain("shelly"); // meter type should be visible
expect(uiContent).not.toContain("testuser@example.com"); // user should be redacted
expect(uiContent).not.toContain("secretpass"); // password should be redacted
expect(uiContent).toContain("***"); // redaction marker should be present
await uiModal.getByRole("button", { name: "Close" }).first().click();
await expectModalHidden(uiModal);
@ -133,7 +151,7 @@ test.describe("issue creation", () => {
await expect(textarea).toBeVisible();
const textareaContent = await textarea.inputValue();
expect(textareaContent).toContain("carport_pv"); // from evcc.yaml
expect(textareaContent).toContain("BigBlueBattery"); // from ui config
expect(textareaContent).toContain("shelly"); // from ui config
expect(textareaContent).toContain("DEBUG"); // from logs
expect(textareaContent).toContain('"telemetry":'); // from state
@ -142,7 +160,7 @@ test.describe("issue creation", () => {
.getByRole("link", { name: "Create GitHub Issue" })
.getAttribute("href");
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
expect(href).not.toContain("BigBlueBattery"); // from ui config
expect(href).not.toContain("TestShelly"); // from ui config
expect(href).not.toContain("carport_pv"); // from evcc.yaml
// close modal
@ -167,7 +185,7 @@ test.describe("issue creation", () => {
.getByRole("link", { name: "Create GitHub Issue" })
.getAttribute("href");
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
expect(href).toContain("BigBlueBattery"); // from ui config
expect(href).toContain("shelly"); // from ui config
expect(href).toContain("carport_pv"); // from evcc.yaml
expect(href).toContain("DEBUG"); // from logs
expect(href).toContain("MyFancyState"); // from state

View file

@ -1,39 +0,0 @@
package util
import (
"fmt"
"maps"
"regexp"
"slices"
"strings"
)
// configRedactSecrets defines keys that should be redacted from configuration files
var configRedactSecrets = []string{
"mac", // infrastructure
"sponsortoken", "plant", // global settings
"apikey", "user", "password", "pin", // users
"token", "access", "refresh", "accesstoken", "refreshtoken", // tokens, including template variations
"ain", "secret", "serial", "deviceid", "machineid", "idtag", // devices
"app", "chats", "recipients", // push messaging
"vin", // vehicles
"lat", "lon", "zip", // solar forecast
}
var configRedactRegex = regexp.MustCompile(fmt.Sprintf(`(?i)\b(%s)\b.*?:.*`, strings.Join(configRedactSecrets, "|")))
// RedactConfigString redacts a configuration string by replacing sensitive values with *****
func RedactConfigString(src string) string {
return configRedactRegex.ReplaceAllString(src, "$1: *****")
}
// RedactConfigMap redacts sensitive keys in a configuration map
func RedactConfigMap(src map[string]any) map[string]any {
res := maps.Clone(src)
for k := range res {
if slices.Contains(configRedactSecrets, k) {
res[k] = "*****"
}
}
return res
}

64
util/redact/redactor.go Normal file
View file

@ -0,0 +1,64 @@
package redact
import (
"fmt"
"maps"
"regexp"
"slices"
"strings"
"github.com/evcc-io/evcc/util/templates"
"github.com/samber/lo"
)
var (
configRedactRegex *regexp.Regexp
configRedactSecrets []string
)
func init() {
// fields that are not covered by template params (yet)
additional := []string{
"sponsortoken", "plant", // global settings
"app", "chats", "recipients", // push messaging
}
// Combine generated params with additional fields
configRedactSecrets = slices.Concat(redactableParams(), additional)
configRedactRegex = regexp.MustCompile(fmt.Sprintf(`(?i)\b(%s)\b.*?:.*`, strings.Join(configRedactSecrets, "|")))
}
func redactableParams() []string {
// Collect all sensitive params from templates (includes defaults)
var params []string
for _, class := range templates.ClassValues() {
for _, tmpl := range templates.ByClass(class) {
for _, p := range tmpl.Params {
if p.IsMasked() || p.IsPrivate() {
params = append(params, strings.ToLower(p.Name))
}
}
}
}
return lo.Uniq(params)
}
// String redacts a configuration string by replacing sensitive values with *****
func String(src string) string {
return configRedactRegex.ReplaceAllString(src, "$1: *****")
}
// Map redacts sensitive keys in a configuration map
func Map(src map[string]any) map[string]any {
res := maps.Clone(src)
for k := range res {
if slices.ContainsFunc(configRedactSecrets, func(s string) bool {
return strings.EqualFold(k, s)
}) {
res[k] = "*****"
}
}
return res
}

View file

@ -0,0 +1,87 @@
package redact
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
tests := []struct {
name string
input string
expected []string // Strings that should appear
redacted []string // Strings that should NOT appear
}{
{
name: "redact password",
input: `site:
title: My Home
meters:
grid: my-grid
user: testuser
password: secretpass123`,
expected: []string{"password: *****", "title: My Home", "grid: my-grid"},
redacted: []string{"secretpass123"},
},
{
name: "redact params marked as private",
input: `vehicle:
vin: W1234567890123456
user: john@example.com
capacity: 50`,
expected: []string{"vin: *****", "user: *****", "capacity: 50"},
redacted: []string{"W1234567890123456", "john@example.com"},
},
{
name: "redact multiple sensitive fields",
input: `config:
lat: 52.520008
lon: 13.404954
zip: 10115
sponsortoken: abc123
apikey: xyz789`,
expected: []string{"lat: *****", "lon: *****", "zip: *****", "sponsortoken: *****", "apikey: *****"},
redacted: []string{"52.520008", "13.404954", "10115", "abc123", "xyz789"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := String(tt.input)
// Check expected strings are present
for _, exp := range tt.expected {
assert.Contains(t, result, exp, "Expected to find %q in result", exp)
}
// Check redacted strings are NOT present
for _, red := range tt.redacted {
assert.NotContains(t, result, red, "Expected %q to be redacted", red)
}
})
}
}
func TestMap(t *testing.T) {
input := map[string]any{
"title": "My Home",
"password": "secret123",
"user": "john@example.com",
"vin": "W1234567890",
"capacity": 50,
"apikey": "abc-def-123",
}
result := Map(input)
// Check non-sensitive fields are unchanged
assert.Equal(t, "My Home", result["title"], "title should not be redacted")
assert.Equal(t, 50, result["capacity"], "capacity should not be redacted")
// Check sensitive fields are redacted
sensitiveFields := []string{"password", "user", "vin", "apikey"}
for _, field := range sensitiveFields {
assert.Equal(t, "*****", result[field], "%s should be redacted", field)
}
}

View file

@ -43,6 +43,7 @@ params:
choice: ["https", "http"]
default: https
- name: user
private: true
description:
de: Benutzerkonto
en: Username
@ -76,6 +77,7 @@ params:
usages: ["pv"]
advanced: true
- name: vin
private: true
description:
de: Fahrzeugidentifikationsnummer
en: Vehicle Identification Number
@ -247,6 +249,7 @@ params:
- name: ski
required: true
private: true
description:
en: Subject Key Identifier (SKI)
de: Identifikationsschlüssel (SKI)
@ -254,16 +257,18 @@ params:
en: Usually found on the web interface of the wallbox
de: Üblicherweise im Web Interface der Wallbox zu finden
- name: uri
private: true
description:
generic: URI
help:
en: HTTP(S) address
de: HTTP(S) Adresse
- name: url
private: true
description:
generic: URL
- name: ain
mask: true
private: true
example: "307788992233"
description:
en: Actor Identification Number (AIN)
@ -340,12 +345,15 @@ params:
advanced: true
- name: accesstoken
mask: true
description:
generic: Access token
- name: refreshtoken
mask: true
description:
generic: Refresh token
- name: serial
private: true
description:
en: Serial
de: Seriennummer
@ -360,9 +368,11 @@ params:
description:
generic: Client ID
- name: clientsecret
mask: true
description:
generic: Client Secret
- name: token
mask: true
description:
generic: Token
- name: interval
@ -370,13 +380,16 @@ params:
de: Intervall
en: Interval
- name: mac
private: true
description:
en: MAC Address
de: MAC Adresse
- name: pin
mask: true
description:
generic: PIN
- name: serial_number
private: true
description:
en: Serial Number
de: Seriennummer
@ -384,9 +397,11 @@ params:
description:
generic: Watchdog
- name: uuid
private: true
description:
generic: UUID
- name: zip
private: true
description:
en: ZIP code
de: Postleitzahl
@ -420,12 +435,14 @@ params:
de: Temperaturquelle
en: Temperature source
- name: lat
private: true
description:
en: Latitude
de: Breitengrad
type: float
example: 55.7351
- name: lon
private: true
description:
en: Longitude
de: Längengrad
@ -601,6 +618,7 @@ presets:
en: Only enable this option if there is no way to initiate transactions from the charger side! This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! (Uses OCPP RemoteStartTransaction)
- name: idtag
advanced: true
private: true
type: string
description:
en: Authentication token

View file

@ -62,6 +62,11 @@ func (t *Template) Validate() error {
continue
}
// Validate that a param cannot be both masked and private
if p.Mask && p.Private {
return fmt.Errorf("param %s: 'mask' and 'private' cannot be used together. Use 'mask' for sensitive data like passwords/tokens that should be hidden in UI. Use 'private' for personal data like emails/locations that should only be redacted from bug reports", p.Name)
}
if p.Description.String("en") == "" || p.Description.String("de") == "" {
return fmt.Errorf("param %s: description can't be empty", p.Name)
}

View file

@ -189,6 +189,7 @@ type Param struct {
Preset string `json:"-"` // Reference a predefined set of params
Required bool `json:",omitempty"` // cli if the user has to provide a non empty value
Mask bool `json:",omitempty"` // cli if the value should be masked, e.g. for passwords
Private bool `json:",omitempty"` // value should be redacted in bug reports, e.g. email, locations, ...
Advanced bool `json:",omitempty"` // cli if the user does not need to be asked. Requires a "Default" to be defined.
Deprecated bool `json:",omitempty"` // if the parameter is deprecated and thus should not be presented in the cli or docs
Default string `json:",omitempty"` // default value if no user value is provided in the configuration
@ -238,6 +239,10 @@ func (p *Param) IsMasked() bool {
return p.Mask
}
func (p *Param) IsPrivate() bool {
return p.Private
}
func (p *Param) IsRequired() bool {
return p.Required
}