From a2975d824a2b043f8e9c24a2bacb475192756046 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 17 Jul 2025 12:39:21 +0200 Subject: [PATCH] Backup Restore: clarify password (#22411) --- assets/js/components/Auth/PasswordInput.vue | 5 ++--- assets/js/components/Config/BackupRestoreModal.vue | 5 ++++- assets/js/views/Config.vue | 7 ++++++- cmd/root.go | 1 + core/keys/global.go | 1 + i18n/de.json | 4 +--- i18n/en.json | 4 +--- server/http_site_handler.go | 11 ++++++++--- tests/auth.spec.ts | 12 ++++++------ tests/backup-restore.spec.ts | 7 +++++-- tests/config-onboarding.spec.ts | 2 +- 11 files changed, 36 insertions(+), 23 deletions(-) diff --git a/assets/js/components/Auth/PasswordInput.vue b/assets/js/components/Auth/PasswordInput.vue index 88265c6ee..71bea86cb 100644 --- a/assets/js/components/Auth/PasswordInput.vue +++ b/assets/js/components/Auth/PasswordInput.vue @@ -13,12 +13,12 @@ class="form-control" autocomplete="current-password" type="password" - :required="required" + required @input="updatePassword" /> -

{{ $t("loginModal.error") }}{{ error }}

+

{{ error }}

@@ -180,6 +180,9 @@ const validateStatus = (code: number) => [200, 204, 401, 403].includes(code); export default defineComponent({ name: "BackupRestoreModal", components: { GenericModal, PropertyFileField, FormRow, PasswordInput }, + props: { + authDisabled: Boolean, + }, data() { return { selectedReset: { diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index 5ef814e93..1d78d6820 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -373,7 +373,7 @@ - +
@@ -598,6 +598,11 @@ export default { messagingTags() { return { configured: { value: store.state?.messaging || false } }; }, + backupRestoreProps() { + return { + authDisabled: store.state?.authDisabled || false, + }; + }, }, watch: { offline() { diff --git a/cmd/root.go b/cmd/root.go index a087e5f4b..625dffea6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -320,6 +320,7 @@ func runRoot(cmd *cobra.Command, args []string) { if ok, _ := cmd.Flags().GetBool(flagDisableAuth); ok { log.WARN.Println("❗❗❗ Authentication is disabled. This is dangerous. Your data and credentials are not protected.") authObject.SetAuthMode(auth.Disabled) + valueChan <- util.Param{Key: keys.AuthDisabled, Val: true} } if ok, _ := cmd.Flags().GetBool(flagDemoMode); ok { diff --git a/core/keys/global.go b/core/keys/global.go index 4315cd9c0..c8c6647bf 100644 --- a/core/keys/global.go +++ b/core/keys/global.go @@ -19,4 +19,5 @@ const ( Plant = "plant" Telemetry = "telemetry" DemoMode = "demoMode" + AuthDisabled = "authDisabled" ) diff --git a/i18n/de.json b/i18n/de.json index 463134c2f..446a97525 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -447,7 +447,6 @@ "title": "Sichern" }, "cancel": "Abbrechen", - "confirmWithPassword": "Aktion bestätigen", "description": "Sichern, wiederherstellen und zurücksetzen deiner Daten. Nützlich, wenn du deine Daten auf ein anderes System übertragen möchtest.", "note": "Hinweis: Alle oben genannten Aktionen betreffen nur deine Datenbankdaten. Die evcc.yaml-Konfigurationsdatei bleibt unverändert.", "reset": { @@ -665,12 +664,11 @@ "loginModal": { "cancel": "Abbrechen", "demoMode": "Login ist im Demo-Modus nicht verfügbar.", - "error": "Login fehlgeschlagen: ", "iframeHint": "Öffne evcc in einem neuen Tab.", "iframeIssue": "Das Passwort ist korrekt, aber dein Browser hat das Authentifizierungscookie abgelehnt. Dies kann passieren, wenn du evcc in einem iframe über HTTP verwendest.", "invalid": "Passwort ist ungültig.", "login": "Anmelden", - "password": "Passwort", + "password": "Administrator Passwort", "reset": "Passwort zurücksetzen?", "title": "Authentifizierung" }, diff --git a/i18n/en.json b/i18n/en.json index 85dbdba44..709af452e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -447,7 +447,6 @@ "title": "Backup" }, "cancel": "Cancel", - "confirmWithPassword": "Confirm action", "description": "Backup, restore and reset your data. Useful if you want to move your data to another system.", "note": "Note: All above actions only affect your database data. The evcc.yaml configuration file remains unchanged.", "reset": { @@ -665,12 +664,11 @@ "loginModal": { "cancel": "Cancel", "demoMode": "Login is not supported in demo mode.", - "error": "Login failed: ", "iframeHint": "Open evcc in a new tab.", "iframeIssue": "Your password is correct, but your browser seems to have dropped the authentication cookie. This can happen if you run evcc in an iframe via HTTP.", "invalid": "Password is invalid.", "login": "Login", - "password": "Password", + "password": "Administrator Password", "reset": "Reset password?", "title": "Authentication" }, diff --git a/server/http_site_handler.go b/server/http_site_handler.go index e9ad4fdef..ff8fb98e3 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -354,6 +354,11 @@ func logHandler(w http.ResponseWriter, r *http.Request) { jsonResult(w, log) } +// adminPasswordValid validates the admin password and returns true if valid +func adminPasswordValid(authObject auth.Auth, password string) bool { + return authObject.GetAuthMode() == auth.Disabled || authObject.IsAdminPasswordValid(password) +} + func getBackup(authObject auth.Auth) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req loginRequest @@ -362,7 +367,7 @@ func getBackup(authObject auth.Auth) http.HandlerFunc { return } - if authObject.GetAuthMode() == auth.Enabled && !authObject.IsAdminPasswordValid(req.Password) { + if !adminPasswordValid(authObject, req.Password) { http.Error(w, "Invalid password", http.StatusUnauthorized) return } @@ -422,7 +427,7 @@ func restoreDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc { return } - if authObject.GetAuthMode() == auth.Enabled && !authObject.IsAdminPasswordValid(r.FormValue("password")) { + if !adminPasswordValid(authObject, r.FormValue("password")) { http.Error(w, "Invalid password", http.StatusUnauthorized) return } @@ -479,7 +484,7 @@ func resetDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc { return } - if authObject.GetAuthMode() == auth.Enabled && !authObject.IsAdminPasswordValid(req.Password) { + if !adminPasswordValid(authObject, req.Password) { http.Error(w, "Invalid password", http.StatusUnauthorized) return } diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts index 2707470d6..608da61dc 100644 --- a/tests/auth.spec.ts +++ b/tests/auth.spec.ts @@ -53,12 +53,12 @@ test("login", async ({ page }) => { await expect(login.getByRole("heading", { name: "Authentication" })).toBeVisible(); // enter wrong password - await login.getByLabel("Password").fill("wrong"); + await login.getByLabel("Administrator Password").fill("wrong"); await login.getByRole("button", { name: "Login" }).click(); - await expect(login.getByText("Login failed: Password is invalid.")).toBeVisible(); + await expect(login.getByText("Password is invalid.")).toBeVisible(); // enter correct password - await login.getByLabel("Password").fill("secret"); + await login.getByLabel("Administrator Password").fill("secret"); await login.getByRole("button", { name: "Login" }).click(); await expectModalHidden(login); await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); @@ -86,7 +86,7 @@ test("http iframe hint", async ({ page }) => { }); // enter correct password - await login.getByLabel("Password").fill("secret"); + await login.getByLabel("Administrator Password").fill("secret"); await login.getByRole("button", { name: "Login" }).click(); // iframe hint visible (login-iframe-hint) @@ -105,7 +105,7 @@ test("update password", async ({ page }) => { await page.goto("/#/config"); const loginModal = page.getByTestId("login-modal"); await expectModalVisible(loginModal); - await loginModal.getByLabel("Password").fill(oldPassword); + await loginModal.getByLabel("Administrator Password").fill(oldPassword); await loginModal.getByRole("button", { name: "Login" }).click(); await expectModalHidden(loginModal); @@ -134,7 +134,7 @@ test("update password", async ({ page }) => { await expectTopNavigationClosed(page); const loginNew = page.getByTestId("login-modal"); await expectModalVisible(loginNew); - await loginNew.getByLabel("Password").fill(newPassword); + await loginNew.getByLabel("Administrator Password").fill(newPassword); await loginNew.getByRole("button", { name: "Login" }).click(); await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); await expectModalHidden(loginNew); diff --git a/tests/backup-restore.spec.ts b/tests/backup-restore.spec.ts index f5f3af689..57c923d54 100644 --- a/tests/backup-restore.spec.ts +++ b/tests/backup-restore.spec.ts @@ -33,6 +33,7 @@ test.describe("reset", async () => { await modal.getByRole("button", { name: "Reset..." }).click(); const confirmModal = page.getByTestId("backup-restore-confirm-modal"); await expectModalVisible(confirmModal); + await expect(confirmModal.getByLabel("Administrator Password")).not.toBeVisible(); // disable auth mode await confirmModal.getByRole("button", { name: "Reset & restart" }).click(); await expectModalHidden(confirmModal); await expectModalHidden(modal); @@ -87,6 +88,7 @@ test.describe("reset", async () => { await modal.getByRole("button", { name: "Reset..." }).click(); const confirmModal = page.getByTestId("backup-restore-confirm-modal"); await expectModalVisible(confirmModal); + await expect(confirmModal.getByLabel("Administrator Password")).not.toBeVisible(); // disable auth mode await confirmModal.getByRole("button", { name: "Reset & restart" }).click(); await expectModalHidden(confirmModal); await expectModalHidden(modal); @@ -232,10 +234,11 @@ test.describe("backup and restore", async () => { await backupModal.getByRole("button", { name: "Download backup..." }).click(); const backupConfirmModal = page.getByTestId("backup-restore-confirm-modal"); await expectModalVisible(backupConfirmModal); - const passwordField = backupConfirmModal.getByLabel("Password"); + const passwordField = backupConfirmModal.getByLabel("Administrator Password"); + await expect(passwordField).toBeVisible(); await passwordField.fill("wrongpassword"); await backupConfirmModal.getByRole("button", { name: "Download backup" }).click(); - await expect(backupConfirmModal.getByText("Login failed: Password is invalid.")).toBeVisible(); + await expect(backupConfirmModal.getByText("Password is invalid.")).toBeVisible(); await passwordField.clear(); await passwordField.fill("secret"); const downloadPromise = page.waitForEvent("download"); diff --git a/tests/config-onboarding.spec.ts b/tests/config-onboarding.spec.ts index 2c75c95f5..716b9567a 100644 --- a/tests/config-onboarding.spec.ts +++ b/tests/config-onboarding.spec.ts @@ -32,7 +32,7 @@ test.describe("onboarding", async () => { // login const login = page.getByTestId("login-modal"); await expectModalVisible(login); - await login.getByLabel("Password").fill(PASSWORD); + await login.getByLabel("Administrator Password").fill(PASSWORD); await login.getByRole("button", { name: "Login" }).click(); await expectModalHidden(login);