// SPDX-License-Identifier: AGPL-3.0-or-later import {useHashParam} from '@app/features/app/hooks/useHashParam'; import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands'; import styles from '@app/features/auth/components/pages/ResetPasswordPage.module.css'; import FormField from '@app/features/auth/flow/AuthFormField'; import {AuthRouterLink} from '@app/features/auth/flow/AuthRouterLink'; import {useAuthForm} from '@app/features/auth/hooks/useAuthForm'; import {resetPassword as resetPasswordFlow} from '@app/features/auth/state/AuthFlow'; import {BACK_TO_SIGN_IN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import * as RouterUtils from '@app/features/navigation/utils/RouterUtils'; import {Button} from '@app/features/ui/button/Button'; import {useFluxerDocumentTitle} from '@app/features/window/hooks/useFluxerDocumentTitle'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; import {useEffect, useId, useState} from 'react'; const RESET_PASSWORD_DESCRIPTOR = msg({ message: 'Reset password', comment: 'Short label in the authentication reset password page. Keep the tone plain and specific.', }); const NEW_PASSWORD_DESCRIPTOR = msg({ message: 'New password', comment: 'Short label in the authentication reset password page. Keep the tone plain and specific.', }); const CONFIRM_NEW_PASSWORD_DESCRIPTOR = msg({ message: 'Confirm new password', comment: 'Short label in the authentication reset password page. Keep the tone plain and specific.', }); type TokenStatus = 'validating' | 'valid' | 'invalid'; const API_RENDERED_FIELDS = new Set(['password']); function resolveBannerError( error: string | null, fieldErrors: ReadonlyMap | null | undefined, ): string | null { if (error != null) { return error; } if (fieldErrors == null) { return null; } for (const [fieldName, message] of fieldErrors) { if (!API_RENDERED_FIELDS.has(fieldName)) { return message; } } return null; } const ResetPasswordPage = observer(function ResetPasswordPage() { const {i18n} = useLingui(); const passwordId = useId(); const confirmPasswordId = useId(); useFluxerDocumentTitle(i18n._(RESET_PASSWORD_DESCRIPTOR)); const token = useHashParam('token'); const [tokenStatus, setTokenStatus] = useState('validating'); const {form, isLoading, error, fieldErrors} = useAuthForm({ initialValues: { password: '', confirmPassword: '', }, onSubmit: async (values) => { if (!token) { form.setError('password', 'Invalid or missing reset token'); return; } if (values.password !== values.confirmPassword) { form.setError('confirmPassword', 'Passwords do not match'); return; } const response = await resetPasswordFlow(token, values.password); if (response.type === 'mfa') { AuthenticationCommands.setMfaTicket({ ticket: response.challenge.ticket, totp: response.challenge.totp, webauthn: response.challenge.webauthn, }); RouterUtils.replaceWith('/login'); return; } await AuthenticationCommands.completeLogin(response.payload); }, }); useEffect(() => { if (!token) { RouterUtils.replaceWith('/forgot'); return; } let cancelled = false; setTokenStatus('validating'); AuthenticationCommands.validateResetPasswordToken(token) .then((valid) => { if (cancelled) return; setTokenStatus(valid ? 'valid' : 'invalid'); }) .catch(() => { if (cancelled) return; setTokenStatus('invalid'); }); return () => { cancelled = true; }; }, [token]); const bannerError = resolveBannerError(error, fieldErrors); if (tokenStatus === 'validating') { return ( <>

Set new password

Verifying your reset link…

); } if (tokenStatus === 'invalid') { return ( <>

Reset link invalid or expired

This reset link has expired. Reset links last 1 hour. Please request a new one.

Request a new reset link
); } return ( <>

Set new password

Set your new password.

{bannerError ? (
{bannerError}
) : null}
form.setValue('password', value)} error={form.getError('password') || fieldErrors?.get('password')} data-flx="auth.reset-password-page.form-field.set-value.password" /> form.setValue('confirmPassword', value)} error={form.getError('confirmPassword')} data-flx="auth.reset-password-page.form-field.set-value.password--2" />
{i18n._(BACK_TO_SIGN_IN_DESCRIPTOR)}
); }); export default ResetPasswordPage;