- "Mein Konto": Geraetemarken anlegen und widerrufen. Der Klartext ist nur einmal zu sehen, daneben steht eine Bauanleitung fuer den Kurzbefehl mit kopierfertiger Adresse und Kopfzeile. Ein fertiger Download geht nicht: seit iOS 15 muessen Kurzbefehl-Dateien signiert sein, und signieren kann nur ein Mac mit Apple-Konto. - Neue Seite "Eingang": was unterwegs erfasst wurde, mit Kategorie-Auswahl und Verwerfen. - Die Import-Pruefliste unterscheidet jetzt, WARUM eine Zeile uebersprungen wird: "schon erfasst" mit Erklaerung statt nur "Duplikat". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
4.1 KiB
React
108 lines
4.1 KiB
React
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 Geraetemarken from "./settings/Geraetemarken";
|
||
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 (
|
||
<div>
|
||
<h1>{t("account.title")}</h1>
|
||
<div className="settings-grid">
|
||
<form className="card" onSubmit={onSubmit}>
|
||
<h2 style={{ marginTop: 0 }}>{t("account.password")}</h2>
|
||
{fehler && (
|
||
<div className="alert error" style={{ marginBottom: "var(--sp-3)" }}>
|
||
<Icon name="alert" size={16} /><span>{fehler}</span>
|
||
</div>
|
||
)}
|
||
<label className="field">
|
||
<span>{t("changepw.current")}</span>
|
||
<PasswordInput value={current} onChange={setCurrent} autoComplete="current-password" required />
|
||
</label>
|
||
<label className="field">
|
||
<span>{t("changepw.new")}</span>
|
||
<PasswordInput value={next} onChange={setNext} autoComplete="new-password" required />
|
||
</label>
|
||
<PasswordRules policy={policy} value={next} />
|
||
<label className="field" style={{ marginTop: "var(--sp-3)" }}>
|
||
<span>{t("changepw.repeat")}</span>
|
||
<PasswordInput value={repeat} onChange={setRepeat} autoComplete="new-password" required />
|
||
</label>
|
||
{repeat && next !== repeat && (
|
||
<span className="hint-danger"><Icon name="alert" size={12} />{t("changepw.mismatch")}</span>
|
||
)}
|
||
<button className="btn primary" type="submit" disabled={!bereit} style={{ marginTop: "var(--sp-3)" }}>
|
||
{busy ? t("common.loading") : t("changepw.submit")}
|
||
</button>
|
||
</form>
|
||
|
||
<div className="card">
|
||
<h2 style={{ marginTop: 0, display: "flex", alignItems: "center", gap: "var(--sp-2)" }}>
|
||
<Icon name="shield" size={18} /> {t("account.twoFactor")}
|
||
</h2>
|
||
<p className="muted small">{t("settings.securityHint")}</p>
|
||
<p>
|
||
<span className={`badge ${tfa?.enabled ? "ok" : ""}`}>
|
||
{tfa?.enabled ? t("twofactor.on") : t("twofactor.off")}
|
||
</span>
|
||
</p>
|
||
<Link className="btn" to="/zwei-faktor">
|
||
<Icon name="smartphone" size={15} /> {tfa?.enabled ? t("account.manage2fa") : t("settings.twoFactor")}
|
||
</Link>
|
||
</div>
|
||
|
||
<Geraetemarken />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|