All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
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 { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
|
import { PageShell } from "../components/PageShell.tsx";
|
|
import {
|
|
expectOk,
|
|
FormError,
|
|
FormProvider,
|
|
SubmitButton,
|
|
TextField,
|
|
useApiForm,
|
|
} from "../components/form/index.ts";
|
|
|
|
interface Values {
|
|
newPassword: string;
|
|
confirm: string;
|
|
}
|
|
|
|
export function ResetPassword() {
|
|
useDocumentTitle(t`Reset password`);
|
|
const [params] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const token = params.get("token") ?? "";
|
|
const [done, setDone] = useState(false);
|
|
|
|
const form = useApiForm<Values>({
|
|
mode: "onChange",
|
|
defaultValues: { newPassword: "", confirm: "" },
|
|
});
|
|
|
|
const onSubmit = form.submit(async ({ newPassword }) => {
|
|
await expectOk(
|
|
await fetch(`${API_URL}/api/users/reset-password`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ token, newPassword }),
|
|
}),
|
|
);
|
|
setDone(true);
|
|
});
|
|
|
|
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 (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>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageShell centered>
|
|
<div className="auth-card">
|
|
<h1 className="auth-card-title">
|
|
<Trans>Set new password</Trans>
|
|
</h1>
|
|
|
|
<FormProvider {...form}>
|
|
<form onSubmit={onSubmit} className="form">
|
|
<FormError title={t`Reset failed`} />
|
|
<TextField<Values>
|
|
name="newPassword"
|
|
type="password"
|
|
placeholder={t`New password`}
|
|
autoComplete="new-password"
|
|
autoFocus
|
|
maxLength={VALIDATION.PASSWORD_MAX}
|
|
rules={{
|
|
required: true,
|
|
minLength: {
|
|
value: VALIDATION.PASSWORD_MIN,
|
|
message: t`At least ${VALIDATION.PASSWORD_MIN} characters`,
|
|
},
|
|
}}
|
|
/>
|
|
<TextField<Values>
|
|
name="confirm"
|
|
type="password"
|
|
placeholder={t`Confirm new password`}
|
|
autoComplete="new-password"
|
|
rules={{
|
|
required: true,
|
|
validate: (value, values) =>
|
|
value === values.newPassword || t`Passwords do not match`,
|
|
}}
|
|
/>
|
|
<SubmitButton pendingLabel={<Trans>Saving…</Trans>}>
|
|
<Trans>Set new password</Trans>
|
|
</SubmitButton>
|
|
</form>
|
|
</FormProvider>
|
|
|
|
<p className="auth-card-footer">
|
|
<Link to="/login">
|
|
<Trans>Back to login</Trans>
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</PageShell>
|
|
);
|
|
}
|