Files
certain/web/src/pages/Account.jsx
T
GuidoandClaude Opus 5 bf656dc957 feat: Oberflaeche fuer die Schnellerfassung
- "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>
2026-08-22 19:39:31 +02:00

108 lines
4.1 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}