132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
/**
|
|
* The password change modal.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { API_URL } from "../../config/api.ts";
|
|
import { VALIDATION } from "../../../shared/validation.ts";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import { Modal } from "../shared/Modal.tsx";
|
|
import { expectOk } from "../../utils/shared/api.ts";
|
|
import { FormActions } from "../form/FormActions.tsx";
|
|
import { FormError } from "../form/FormError.tsx";
|
|
import { FormProvider } from "react-hook-form";
|
|
import { SubmitButton } from "../form/SubmitButton.tsx";
|
|
import { TextField } from "../form/TextField.tsx";
|
|
import { useApiForm } from "../../hooks/form/useApiForm.ts";
|
|
|
|
/** The props of {@link ChangePasswordModal}. */
|
|
export interface ChangePasswordModalProps {
|
|
/** Called to close it. */
|
|
onClose: () => void;
|
|
}
|
|
|
|
/** The form's fields. */
|
|
interface Values {
|
|
currentPassword: string;
|
|
newPassword: string;
|
|
confirmPassword: string;
|
|
}
|
|
|
|
/**
|
|
* Renders the form that changes the user's password, given the current one. The
|
|
* change signs out every other session, and this one keeps the fresh tokens it
|
|
* is handed.
|
|
*/
|
|
export function ChangePasswordModal(
|
|
{ onClose }: ChangePasswordModalProps,
|
|
): React.JSX.Element {
|
|
const { authFetch, updateSession } = useAuth();
|
|
const [done, setDone] = useState(false);
|
|
const form = useApiForm<Values>({
|
|
mode: "onChange",
|
|
defaultValues: {
|
|
currentPassword: "",
|
|
newPassword: "",
|
|
confirmPassword: "",
|
|
},
|
|
});
|
|
|
|
const onSubmit = form.submit(async ({ currentPassword, newPassword }) => {
|
|
// The change signs out every session opened with the old password, this
|
|
// one included, so keep the fresh tokens it hands back.
|
|
const tokens = await expectOk<{ token: string; mediaToken: string }>(
|
|
await authFetch(`${API_URL}/api/users/me/change-password`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ currentPassword, newPassword }),
|
|
}),
|
|
);
|
|
updateSession(tokens);
|
|
setDone(true);
|
|
});
|
|
|
|
return (
|
|
<Modal title={t`Change password`} onClose={onClose}>
|
|
{done
|
|
? (
|
|
<div className="form">
|
|
<p className="form-success">
|
|
<Trans>Password changed successfully.</Trans>
|
|
</p>
|
|
<FormActions>
|
|
<button type="button" className="btn-primary" onClick={onClose}>
|
|
<Trans>Close</Trans>
|
|
</button>
|
|
</FormActions>
|
|
</div>
|
|
)
|
|
: (
|
|
<FormProvider {...form}>
|
|
<form className="form" onSubmit={onSubmit}>
|
|
<TextField<Values>
|
|
name="currentPassword"
|
|
type="password"
|
|
label={t`Current password`}
|
|
autoComplete="current-password"
|
|
autoFocus
|
|
rules={{ required: true }}
|
|
/>
|
|
<TextField<Values>
|
|
name="newPassword"
|
|
type="password"
|
|
label={t`New password`}
|
|
autoComplete="new-password"
|
|
maxLength={VALIDATION.PASSWORD_MAX}
|
|
rules={{
|
|
required: true,
|
|
minLength: {
|
|
value: VALIDATION.PASSWORD_MIN,
|
|
message: t`At least ${VALIDATION.PASSWORD_MIN} characters`,
|
|
},
|
|
}}
|
|
/>
|
|
<TextField<Values>
|
|
name="confirmPassword"
|
|
type="password"
|
|
label={t`Confirm new password`}
|
|
autoComplete="new-password"
|
|
rules={{
|
|
required: true,
|
|
validate: (value, values) =>
|
|
value === values.newPassword ||
|
|
t`Passwords do not match`,
|
|
}}
|
|
/>
|
|
<FormError title={t`Could not change password`} />
|
|
<FormActions onCancel={onClose}>
|
|
<SubmitButton pendingLabel={<Trans>Saving…</Trans>}>
|
|
<Trans>Change password</Trans>
|
|
</SubmitButton>
|
|
</FormActions>
|
|
</form>
|
|
</FormProvider>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|