v3: added password change/reset feature
This commit is contained in:
@@ -16,21 +16,29 @@ import { PageShell } from "../components/PageShell.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
|
||||
type UserLoginState =
|
||||
type LoginState =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "error"; error: string };
|
||||
|
||||
type ResetState =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "sent" }
|
||||
| { status: "error"; error: string };
|
||||
|
||||
export function UserLogin() {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
|
||||
const [state, setState] = useState<UserLoginState>({ status: "idle" });
|
||||
const [loginState, setLoginState] = useState<LoginState>({ status: "idle" });
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
const [resetEmail, setResetEmail] = useState("");
|
||||
const [resetState, setResetState] = useState<ResetState>({ status: "idle" });
|
||||
|
||||
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setState({ status: "submitting" });
|
||||
setLoginState({ status: "submitting" });
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const username = formData.get("username") as string;
|
||||
@@ -49,10 +57,26 @@ export function UserLogin() {
|
||||
login(deserializeAuthResponse(apiResponse.data));
|
||||
navigate("/");
|
||||
} else {
|
||||
setState({ status: "error", error: apiResponse.error.message });
|
||||
setLoginState({ status: "error", error: apiResponse.error.message });
|
||||
}
|
||||
} catch (err) {
|
||||
setState({ status: "error", error: friendlyFetchError(err) });
|
||||
setLoginState({ status: "error", error: friendlyFetchError(err) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetRequest = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!resetEmail.trim()) return;
|
||||
setResetState({ status: "submitting" });
|
||||
try {
|
||||
await fetch(`${API_URL}/api/users/request-password-reset`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: resetEmail.trim() }),
|
||||
});
|
||||
setResetState({ status: "sent" });
|
||||
} catch {
|
||||
setResetState({ status: "error", error: t`Could not connect to server` });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,8 +87,8 @@ export function UserLogin() {
|
||||
<Trans>Log in</Trans>
|
||||
</h1>
|
||||
|
||||
{state.status === "error" && (
|
||||
<ErrorCard title={t`Login failed`} message={state.error} />
|
||||
{loginState.status === "error" && (
|
||||
<ErrorCard title={t`Login failed`} message={loginState.error} />
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
@@ -73,7 +97,7 @@ export function UserLogin() {
|
||||
type="text"
|
||||
placeholder={t`Username`}
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={loginState.status === "submitting"}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
@@ -81,19 +105,81 @@ export function UserLogin() {
|
||||
type="password"
|
||||
placeholder={t`Password`}
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={loginState.status === "submitting"}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={loginState.status === "submitting"}
|
||||
>
|
||||
{state.status === "submitting"
|
||||
{loginState.status === "submitting"
|
||||
? <Trans>Logging in…</Trans>
|
||||
: <Trans>Log in</Trans>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="auth-card-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="auth-link-btn"
|
||||
onClick={() => {
|
||||
setShowReset((v) => !v);
|
||||
setResetState({ status: "idle" });
|
||||
setResetEmail("");
|
||||
}}
|
||||
>
|
||||
<Trans>Forgot password?</Trans>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
{showReset && (
|
||||
<div className="auth-reset-panel">
|
||||
{resetState.status === "sent"
|
||||
? (
|
||||
<p className="auth-reset-sent">
|
||||
<Trans>
|
||||
If that address is registered you'll receive a reset link
|
||||
shortly.
|
||||
</Trans>
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{resetState.status === "error" && (
|
||||
<ErrorCard
|
||||
title={t`Request failed`}
|
||||
message={resetState.error}
|
||||
/>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleResetRequest}
|
||||
className="auth-form auth-reset-form"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
placeholder={t`Your email address`}
|
||||
value={resetEmail}
|
||||
onChange={(e) => setResetEmail(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
disabled={resetState.status === "submitting"}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={resetState.status === "submitting" ||
|
||||
!resetEmail.trim()}
|
||||
>
|
||||
{resetState.status === "submitting"
|
||||
? <Trans>Sending…</Trans>
|
||||
: <Trans>Send reset link</Trans>}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="auth-card-footer">
|
||||
<Trans>This is a mirage.</Trans>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user