From 794a6aeb91b26ccd72f74fd94645c98966f2a1d1 Mon Sep 17 00:00:00 2001 From: Guido Date: Thu, 20 Aug 2026 17:27:40 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Konto-Sicherheit=20geb=C3=BCndelt=20+?= =?UTF-8?q?=20Einstellungen=20aufger=C3=A4umt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Seite „Mein Konto" (/konto, für alle Nutzer): eigenes Passwort ändern (inkl. Richtlinien-Anzeige) + Zwei-Faktor verwalten; erreichbar über den Nutzer-Chip in der Seitenleisten-Fußzeile - Einstellungen: 2FA-Karte entfernt (liegt jetzt bei „Mein Konto"), Karten in ein mehrspaltiges Raster gruppiert - Menüpunkt „Einstellungen" unter die Überschrift „System" verschoben (für alle Nutzer sichtbar; „Benutzer" bleibt Admin-only) Co-Authored-By: Claude Opus 4.8 --- web/src/App.jsx | 14 +- web/src/finance.css | 24 ++++ web/src/locales/de.json | 7 + web/src/locales/en.json | 7 + web/src/pages/Account.jsx | 104 +++++++++++++++ web/src/pages/Settings.jsx | 174 ++++++++++++------------- web/src/pages/settings/PolicyCard.jsx | 4 +- web/src/pages/settings/RestoreCard.jsx | 2 +- 8 files changed, 234 insertions(+), 102 deletions(-) create mode 100644 web/src/pages/Account.jsx diff --git a/web/src/App.jsx b/web/src/App.jsx index 096c77e..846593d 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -15,6 +15,7 @@ import Import from "./pages/Import"; import Users from "./pages/Users"; import Settings from "./pages/Settings"; import TwoFactor from "./pages/TwoFactor"; +import Account from "./pages/Account"; const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link"); @@ -49,19 +50,15 @@ function Sidebar() { +
{t("nav.system")}
- {isAdmin && ( - <> -
{t("nav.system")}
- - - )} + {isAdmin && }
-
+ {user?.display_name || user?.username} {isAdmin ? t("common.admin") : t("common.user")} -
+ @@ -140,6 +137,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/finance.css b/web/src/finance.css index c868b50..932d8ca 100644 --- a/web/src/finance.css +++ b/web/src/finance.css @@ -284,6 +284,30 @@ min-width: 0; } +/* Einstellungen & Mein Konto: Karten in ein mehrspaltiges Raster (nutzt die + Bildschirmbreite besser als das bisherige Untereinander). */ +.settings-grid { + display: grid; + gap: var(--sp-4); + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + align-items: start; +} + +/* Nutzer-Chip in der Seitenleisten-Fußzeile ist ein Link auf „Mein Konto". */ +a.sidebar-user { + text-decoration: none; + border-radius: var(--radius-sm); + padding: 2px 4px; + margin: -2px -4px; + transition: background .12s; +} +a.sidebar-user:hover { + background: var(--surface-2); +} +a.sidebar-user:hover .name { + color: var(--accent); +} + /* Budget: mehrere Monate nebeneinander. */ .budget-months { display: grid; diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 29cb451..1466f04 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -64,8 +64,15 @@ "twoFactorReset": "Zwei-Faktor zurückgesetzt.", "generatePassword": "Passwort erzeugen" }, + "account": { + "title": "Mein Konto", + "password": "Passwort ändern", + "twoFactor": "Zwei-Faktor-Authentifizierung", + "manage2fa": "2FA verwalten" + }, "settings": { "title": "Einstellungen", + "general": "Darstellung & Sprache", "baseCurrency": "Hauptwährung", "baseCurrencyHint": "Währung für Budget-Summen und Dashboard-Gesamtwerte.", "locale": "Sprache", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 733d269..8b8b7ca 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -64,8 +64,15 @@ "twoFactorReset": "Two-factor reset.", "generatePassword": "Generate password" }, + "account": { + "title": "My account", + "password": "Change password", + "twoFactor": "Two-factor authentication", + "manage2fa": "Manage 2FA" + }, "settings": { "title": "Settings", + "general": "Appearance & language", "baseCurrency": "Base currency", "baseCurrencyHint": "Currency for budget totals and dashboard aggregates.", "locale": "Language", diff --git a/web/src/pages/Account.jsx b/web/src/pages/Account.jsx new file mode 100644 index 0000000..127aeef --- /dev/null +++ b/web/src/pages/Account.jsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useAuth } from "../auth"; +import { api, ApiError } from "../api"; +import { useToast } from "../toast"; +import Icon from "../components/Icon"; +import PasswordInput from "../components/PasswordInput"; +import PasswordRules from "../components/PasswordRules"; +import { DEFAULT_POLICY, meetsPolicy } from "../passwordPolicy"; + +/** + * „Mein Konto" – persönlicher Sicherheitsbereich jedes Benutzers: eigenes + * Passwort ändern und Zwei-Faktor verwalten (das eigentliche 2FA-Setup lebt + * unter /zwei-faktor). Fremde Nutzer verwaltet der Admin unter „Benutzer". + */ +export default function Account() { + const { aktualisieren } = useAuth(); + const { t } = useTranslation(); + const toast = useToast(); + + const [current, setCurrent] = useState(""); + const [next, setNext] = useState(""); + const [repeat, setRepeat] = useState(""); + const [policy, setPolicy] = useState(DEFAULT_POLICY); + const [tfa, setTfa] = useState(null); + const [fehler, setFehler] = useState(null); + const [busy, setBusy] = useState(false); + + useEffect(() => { + api.getPolicy().then(setPolicy).catch(() => {}); + api.twoFactorStatus().then(setTfa).catch(() => {}); + }, []); + + const regelnErfuellt = meetsPolicy(policy, next, t); + const bereit = regelnErfuellt && next === repeat && current.length > 0 && !busy; + + async function onSubmit(e) { + e.preventDefault(); + setFehler(null); + if (next !== repeat) { setFehler(t("changepw.mismatch")); return; } + setBusy(true); + try { + const updated = await api.changePassword(current, next); + aktualisieren(updated); + setCurrent(""); setNext(""); setRepeat(""); + toast(t("changepw.saved")); + } catch (err) { + setFehler(err instanceof ApiError ? err.message : t("server.unreachable")); + } finally { + setBusy(false); + } + } + + return ( +
+

{t("account.title")}

+
+
+

{t("account.password")}

+ {fehler && ( +
+ {fehler} +
+ )} + + + + + {repeat && next !== repeat && ( + {t("changepw.mismatch")} + )} + + + +
+

+ {t("account.twoFactor")} +

+

{t("settings.securityHint")}

+

+ + {tfa?.enabled ? t("twofactor.on") : t("twofactor.off")} + +

+ + {tfa?.enabled ? t("account.manage2fa") : t("settings.twoFactor")} + +
+
+
+ ); +} diff --git a/web/src/pages/Settings.jsx b/web/src/pages/Settings.jsx index f4e0ec6..3f55726 100644 --- a/web/src/pages/Settings.jsx +++ b/web/src/pages/Settings.jsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useSettings } from "../settings"; import { useAuth } from "../auth"; @@ -53,7 +52,7 @@ export default function Settings() {

{t("settings.title")}

{flagged.length > 0 && ( -
+

{t("settings.keywordReview")}

@@ -76,101 +75,94 @@ export default function Settings() {
)} -
- -

{t("settings.baseCurrencyHint")}

+
+
+

{t("settings.general")}

+ +

{t("settings.baseCurrencyHint")}

- + - + - -

{t("settings.budgetStartDayHint")}

-
- -
-

- {t("settings.security")} -

-

{t("settings.securityHint")}

- - {t("settings.twoFactor")} - -
- - {isAdmin && } - -
-

{t("settings.backup")}

-

{t("settings.backupHint")}

- - -

{t("settings.exportHint")}

-
- - - + +

{t("settings.budgetStartDayHint")}

- {isAdmin && ( - <> -

{t("settings.dbBackupHint")}

-
- - -
-
- - {t("settings.systemBackupWarning")} -
- - )} -
+ {isAdmin && } - +
+

{t("settings.backup")}

+

{t("settings.backupHint")}

+ + +

{t("settings.exportHint")}

+
+ + + +
+ + {isAdmin && ( + <> +

{t("settings.dbBackupHint")}

+
+ + +
+
+ + {t("settings.systemBackupWarning")} +
+ + )} +
+ + +
); } diff --git a/web/src/pages/settings/PolicyCard.jsx b/web/src/pages/settings/PolicyCard.jsx index 7b95871..324a715 100644 --- a/web/src/pages/settings/PolicyCard.jsx +++ b/web/src/pages/settings/PolicyCard.jsx @@ -53,7 +53,7 @@ export default function PolicyCard() { if (!policy) { return ( -
+

{t("policy.title")}

{fehler ?
{fehler}
:

{t("common.loading")}

} @@ -62,7 +62,7 @@ export default function PolicyCard() { } return ( -
+

{t("policy.title")}

diff --git a/web/src/pages/settings/RestoreCard.jsx b/web/src/pages/settings/RestoreCard.jsx index 975b93b..6969407 100644 --- a/web/src/pages/settings/RestoreCard.jsx +++ b/web/src/pages/settings/RestoreCard.jsx @@ -83,7 +83,7 @@ export default function RestoreCard() { } return ( -
+

{t("restore.title")}