feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)
Grundgerüst für Certain Finance: - Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode), argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin, Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer. - Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus, inkl. SPA-Deeplink-Fallback. - Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN, Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter, Benutzer- und Einstellungsseiten. - Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { useState } from "react";
|
||||
import { Navigate, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../auth";
|
||||
import { api, ApiError } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import BrandMark from "../components/BrandMark";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const { user, aktualisieren } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [current, setCurrent] = useState("");
|
||||
const [next, setNext] = useState("");
|
||||
const [repeat, setRepeat] = useState("");
|
||||
const [fehler, setFehler] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
const forced = user.must_change_password;
|
||||
|
||||
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);
|
||||
toast(t("changepw.saved"));
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
setFehler(err instanceof ApiError ? err.message : t("server.unreachable"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-screen">
|
||||
<form className="card auth-card" onSubmit={onSubmit}>
|
||||
<BrandMark size={40} />
|
||||
<h1 style={{ textAlign: "center", marginTop: 0 }}>{t("changepw.title")}</h1>
|
||||
{forced && (
|
||||
<div className="alert" style={{ marginBottom: "var(--sp-3)" }}>
|
||||
<Icon name="key" size={16} />
|
||||
<span>{t("changepw.forcedHint")}</span>
|
||||
</div>
|
||||
)}
|
||||
{fehler && (
|
||||
<div className="alert error" style={{ marginBottom: "var(--sp-3)" }}>
|
||||
<Icon name="alert" size={16} />
|
||||
<span>{fehler}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label>
|
||||
<span>{t("changepw.current")}</span>
|
||||
<input type="password" autoComplete="current-password" value={current}
|
||||
onChange={(e) => setCurrent(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("changepw.new")}</span>
|
||||
<input type="password" autoComplete="new-password" value={next}
|
||||
onChange={(e) => setNext(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("changepw.repeat")}</span>
|
||||
<input type="password" autoComplete="new-password" value={repeat}
|
||||
onChange={(e) => setRepeat(e.target.value)} required />
|
||||
</label>
|
||||
|
||||
<button className="btn primary" type="submit" disabled={busy}
|
||||
style={{ width: "100%", marginTop: "var(--sp-2)" }}>
|
||||
{busy ? t("common.loading") : t("changepw.submit")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user