v3: added password change/reset feature
This commit is contained in:
165
src/pages/ResetPassword.tsx
Normal file
165
src/pages/ResetPassword.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
import { API_URL, VALIDATION } from "../config/api.ts";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
|
||||
type State =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "done" }
|
||||
| { status: "error"; error: string };
|
||||
|
||||
export function ResetPassword() {
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const token = params.get("token") ?? "";
|
||||
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [state, setState] = useState<State>({ status: "idle" });
|
||||
|
||||
const mismatch = confirm.length > 0 && newPassword !== confirm;
|
||||
const tooShort = newPassword.length > 0 &&
|
||||
newPassword.length < VALIDATION.PASSWORD_MIN;
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
<h1 className="auth-card-title">
|
||||
<Trans>Invalid link</Trans>
|
||||
</h1>
|
||||
<p>
|
||||
<Trans>This reset link is missing or malformed.</Trans>
|
||||
</p>
|
||||
<Link
|
||||
to="/login"
|
||||
className="btn-primary"
|
||||
style={{ marginTop: "1rem", display: "inline-block" }}
|
||||
>
|
||||
<Trans>Back to login</Trans>
|
||||
</Link>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "done") {
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
<h1 className="auth-card-title">
|
||||
<Trans>Password updated</Trans>
|
||||
</h1>
|
||||
<p style={{ marginBottom: "1rem" }}>
|
||||
<Trans>Your password has been changed. You can now log in.</Trans>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
<Trans>Go to login</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (mismatch || tooShort || !newPassword) return;
|
||||
|
||||
setState({ status: "submitting" });
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/users/reset-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token, newPassword }),
|
||||
});
|
||||
const body = await res.json();
|
||||
if (body.success) {
|
||||
setState({ status: "done" });
|
||||
} else {
|
||||
setState({
|
||||
status: "error",
|
||||
error: body.error?.message ?? t`Unknown error`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setState({ status: "error", error: t`Could not connect to server` });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
<h1 className="auth-card-title">
|
||||
<Trans>Set new password</Trans>
|
||||
</h1>
|
||||
|
||||
{state.status === "error" && (
|
||||
<ErrorCard title={t`Reset failed`} message={state.error} />
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder={t`New password`}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
minLength={VALIDATION.PASSWORD_MIN}
|
||||
maxLength={VALIDATION.PASSWORD_MAX}
|
||||
required
|
||||
autoFocus
|
||||
disabled={state.status === "submitting"}
|
||||
/>
|
||||
{tooShort && (
|
||||
<span className="auth-field-hint auth-field-hint--error">
|
||||
<Trans>At least {VALIDATION.PASSWORD_MIN} characters</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder={t`Confirm new password`}
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
/>
|
||||
{mismatch && (
|
||||
<span className="auth-field-hint auth-field-hint--error">
|
||||
<Trans>Passwords do not match</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={state.status === "submitting" || mismatch || tooShort ||
|
||||
!newPassword || !confirm}
|
||||
>
|
||||
{state.status === "submitting"
|
||||
? <Trans>Saving…</Trans>
|
||||
: <Trans>Set new password</Trans>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="auth-card-footer">
|
||||
<Link to="/login">
|
||||
<Trans>Back to login</Trans>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user