Files
certain/web/src/pages/settings/RestoreCard.jsx
T
GuidoandClaude Opus 4.8 794a6aeb91 feat: Konto-Sicherheit gebündelt + Einstellungen aufgeräumt
- 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 <noreply@anthropic.com>
2026-08-20 17:27:40 +02:00

170 lines
5.5 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 { useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { api } from "../../api";
import { useAuth } from "../../auth";
import { useConfirm } from "../../confirm";
import { useToast } from "../../toast";
import Icon from "../../components/Icon";
/**
* Backup zurückladen – zweistufig wie der CSV-Import: erst prüfen lassen, was
* in der Datei steckt, dann bewusst bestätigen. Der Server schreibt beim Prüfen
* nichts.
*/
export default function RestoreCard() {
const { t } = useTranslation();
const toast = useToast();
const confirm = useConfirm();
const navigate = useNavigate();
const { logout } = useAuth();
const dateiFeld = useRef(null);
const [datei, setDatei] = useState(null);
const [vorschau, setVorschau] = useState(null);
const [modus, setModus] = useState("replace");
const [fehler, setFehler] = useState(null);
const [busy, setBusy] = useState(false);
function zuruecksetzen() {
setDatei(null);
setVorschau(null);
setFehler(null);
if (dateiFeld.current) dateiFeld.current.value = "";
}
async function pruefen(f) {
setDatei(f);
setVorschau(null);
setFehler(null);
if (!f) return;
setBusy(true);
try {
setVorschau(await api.restorePreview(f));
} catch (err) {
setFehler(err.message);
} finally {
setBusy(false);
}
}
async function einspielen() {
const systemweit = vorschau.scope === "system";
if (modus === "replace") {
const ok = await confirm({
title: t("restore.confirmTitle"),
message: systemweit ? t("restore.confirmSystem") : t("restore.confirmReplace"),
confirmLabel: t("restore.start"),
danger: true,
});
if (!ok) return;
}
setBusy(true);
setFehler(null);
try {
const res = await api.restoreBackup(datei, systemweit ? "replace" : modus);
if (res.logout_required) {
// Die eigene Sitzung wurde beim Ersetzen der Benutzertabelle mit
// gelöscht – sauber abmelden statt in 401er zu laufen.
toast(t("restore.doneSystem"));
await logout();
navigate("/login");
return;
}
toast(t("restore.done"));
zuruecksetzen();
// Konten, Kategorien und Einstellungen sind komplett andere – neu laden
// ist ehrlicher als jeden Zwischenspeicher einzeln zu erneuern.
window.location.reload();
} catch (err) {
setFehler(err.message);
setBusy(false);
}
}
return (
<div className="card">
<h2 style={{ display: "flex", alignItems: "center", gap: "var(--sp-2)", marginTop: 0 }}>
<Icon name="upload" size={18} /> {t("restore.title")}
</h2>
<p className="muted small">{t("restore.hint")}</p>
{fehler && (
<div className="alert error" style={{ marginBottom: "var(--sp-3)" }}>
<Icon name="alert" size={16} /><span>{fehler}</span>
</div>
)}
<label>
<span>{t("restore.file")}</span>
<input ref={dateiFeld} type="file" accept="application/json,.json"
onChange={(e) => pruefen(e.target.files?.[0] || null)} />
</label>
{busy && !vorschau && <p className="muted small">{t("common.loading")}</p>}
{vorschau && (
<>
<div className="alert" style={{ marginTop: "var(--sp-3)" }}>
<Icon name="check" size={16} />
<div>
<div>
{vorschau.scope === "system"
? t("restore.scopeSystem", { n: vorschau.user_count })
: t("restore.scopeUser", { name: vorschau.username || "?" })}
</div>
<div className="muted small" style={{ marginTop: 2 }}>
{t("restore.exportedAt", { when: vorschau.exported_at || "?" })}
</div>
</div>
</div>
<table className="restore-summary">
<tbody>
{vorschau.tables.map((z) => (
<tr key={z.key}>
<td>{z.label}</td>
<td>{z.count}</td>
</tr>
))}
{vorschau.tables.length === 0 && (
<tr><td colSpan={2} className="muted">{t("restore.empty")}</td></tr>
)}
</tbody>
</table>
{vorschau.notes.map((n) => (
<div className="alert warn" key={n} style={{ marginTop: "var(--sp-3)" }}>
<Icon name="alert" size={16} /><span>{n}</span>
</div>
))}
{vorschau.scope === "user" && (
<label style={{ marginTop: "var(--sp-3)" }}>
<span>{t("restore.mode")}</span>
<select value={modus} onChange={(e) => setModus(e.target.value)}>
<option value="replace">{t("restore.modeReplace")}</option>
<option value="merge">{t("restore.modeMerge")}</option>
</select>
</label>
)}
<p className="muted small">
{vorschau.scope === "system" || modus === "replace"
? t("restore.modeReplaceHint")
: t("restore.modeMergeHint")}
</p>
<div className="btn-pair">
<button className="btn danger-solid" disabled={busy} onClick={einspielen}>
{busy ? t("common.loading") : t("restore.start")}
</button>
<button className="btn ghost" disabled={busy} onClick={zuruecksetzen}>
{t("common.cancel")}
</button>
</div>
</>
)}
</div>
);
}