Passwort-Reset per Einmal-Link (kver reset-link)
Ohne E-Mail-System erzeugt der Admin per CLI einen Link (/reset#<token>), gültig 72 h und einmal. Gespeichert wird nur der SHA-256 des Tokens; es steht im Fragment und geht nur im POST-Body an die API. Der Reset meldet alle alten Sessions ab und loggt direkt neu ein. Passwortregeln jetzt gemeinsam in checkNewPassword. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b6cf8c7fa0
commit
81a74375b7
@@ -8,6 +8,7 @@ import { FeedPage } from "./pages/FeedPage";
|
||||
import { EntryPage } from "./pages/EntryPage";
|
||||
import { UserPage } from "./pages/UserPage";
|
||||
import { StatsPage } from "./pages/StatsPage";
|
||||
import { ResetPage } from "./pages/ResetPage";
|
||||
import "./app.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -31,6 +32,7 @@ const router = createBrowserRouter([
|
||||
{ path: "/e/:pid", element: <EntryPage /> },
|
||||
{ path: "/u/:username", element: <UserPage /> },
|
||||
{ path: "/stats", element: <StatsPage /> },
|
||||
{ path: "/reset", element: <ResetPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, errMsg } from "../api";
|
||||
import { useAuthChanged } from "../auth";
|
||||
|
||||
// ResetPage: neues Passwort über einen Reset-Link (/reset#<token>, erzeugt per
|
||||
// kver reset-link). Das Token steht im Fragment, damit es nie in einer URL beim
|
||||
// Server landet; es geht nur im POST-Body raus.
|
||||
export function ResetPage() {
|
||||
const [token] = useState(() => window.location.hash.slice(1));
|
||||
const [msg, setMsg] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const authChanged = useAuthChanged();
|
||||
|
||||
const check = useQuery({
|
||||
queryKey: ["reset", token],
|
||||
queryFn: () => {
|
||||
const fd = new FormData();
|
||||
fd.append("token", token);
|
||||
return api.post<{ username: string }>("/auth/reset/check", fd);
|
||||
},
|
||||
enabled: token !== "",
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
// Nach Erfolg ist man eingeloggt -> ab in den Feed.
|
||||
const submit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
fd.append("token", token);
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.post("/auth/reset", fd);
|
||||
await authChanged();
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
setMsg(errMsg(err, "Passwort konnte nicht gesetzt werden."));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
let error = "";
|
||||
if (token === "") error = "Der Link ist unvollständig.";
|
||||
else if (check.isError) error = errMsg(check.error, "Der Link konnte nicht geprüft werden.");
|
||||
|
||||
return (
|
||||
<section id="reset">
|
||||
<h2>Passwort neu setzen</h2>
|
||||
{error && <p className="msg">{error}</p>}
|
||||
{check.data && (
|
||||
<form id="reset-form" onSubmit={submit}>
|
||||
<p>für <strong>{check.data.username}</strong></p>
|
||||
<input name="username" type="hidden" value={check.data.username} autoComplete="username" />
|
||||
<input name="pass1" type="password" placeholder="Neues Passwort" autoComplete="new-password" />{" "}
|
||||
<input name="pass2" type="password" placeholder="Passwort wiederholen" autoComplete="new-password" />{" "}
|
||||
<button type="submit" disabled={busy}>Passwort setzen</button>
|
||||
</form>
|
||||
)}
|
||||
<p className="msg" id="reset-msg">{msg}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user