New password reset flow (#9581)
This commit is contained in:
parent
3f74ac37e8
commit
e5ce6d7800
23 changed files with 1163 additions and 362 deletions
|
@ -16,104 +16,102 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import React, { ReactNode } from 'react';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import EmailField from "../../views/auth/EmailField";
|
||||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
import InlineSpinner from '../../views/elements/InlineSpinner';
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
import QuestionDialog from "../../views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AuthBody from "../../views/auth/AuthBody";
|
||||
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import StyledCheckbox from '../../views/elements/StyledCheckbox';
|
||||
import { ValidatedServerConfig } from '../../../utils/ValidatedServerConfig';
|
||||
import { Icon as LockIcon } from "../../../../res/img/element-icons/lock.svg";
|
||||
import QuestionDialog from '../../views/dialogs/QuestionDialog';
|
||||
import { EnterEmail } from './forgot-password/EnterEmail';
|
||||
import { CheckEmail } from './forgot-password/CheckEmail';
|
||||
import Field from '../../views/elements/Field';
|
||||
import { ErrorMessage } from '../ErrorMessage';
|
||||
import { Icon as CheckboxIcon } from "../../../../res/img/element-icons/Checkbox.svg";
|
||||
import { VerifyEmailModal } from './forgot-password/VerifyEmailModal';
|
||||
import Spinner from '../../views/elements/Spinner';
|
||||
import { formatSeconds } from '../../../DateUtils';
|
||||
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
||||
|
||||
enum Phase {
|
||||
// Show the forgot password inputs
|
||||
Forgot = 1,
|
||||
// Show email input
|
||||
EnterEmail = 1,
|
||||
// Email is in the process of being sent
|
||||
SendingEmail = 2,
|
||||
// Email has been sent
|
||||
EmailSent = 3,
|
||||
// User has clicked the link in email and completed reset
|
||||
Done = 4,
|
||||
// Show new password input
|
||||
PasswordInput = 4,
|
||||
// Password is in the process of being reset
|
||||
ResettingPassword = 5,
|
||||
// All done
|
||||
Done = 6,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
onServerConfigChange: (serverConfig: ValidatedServerConfig) => void;
|
||||
onLoginClick?: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
interface State {
|
||||
phase: Phase;
|
||||
email: string;
|
||||
password: string;
|
||||
password2: string;
|
||||
errorText: string;
|
||||
errorText: string | ReactNode | null;
|
||||
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: boolean;
|
||||
serverErrorIsFatal: boolean;
|
||||
serverDeadError: string;
|
||||
|
||||
currentHttpRequest?: Promise<any>;
|
||||
|
||||
serverSupportsControlOfDevicesLogout: boolean;
|
||||
logoutDevices: boolean;
|
||||
}
|
||||
|
||||
enum ForgotPasswordField {
|
||||
Email = 'field_email',
|
||||
Password = 'field_password',
|
||||
PasswordConfirm = 'field_password_confirm',
|
||||
}
|
||||
|
||||
export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||
export default class ForgotPassword extends React.Component<Props, State> {
|
||||
private reset: PasswordReset;
|
||||
private fieldPassword: Field | null = null;
|
||||
private fieldPasswordConfirm: Field | null = null;
|
||||
|
||||
state: IState = {
|
||||
phase: Phase.Forgot,
|
||||
email: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
errorText: null,
|
||||
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
serverSupportsControlOfDevicesLogout: false,
|
||||
logoutDevices: false,
|
||||
};
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
phase: Phase.EnterEmail,
|
||||
email: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
errorText: null,
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverDeadError: "",
|
||||
serverSupportsControlOfDevicesLogout: false,
|
||||
logoutDevices: false,
|
||||
};
|
||||
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.reset = null;
|
||||
this.checkServerLiveliness(this.props.serverConfig);
|
||||
this.checkServerCapabilities(this.props.serverConfig);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
public componentDidUpdate(prevProps: Readonly<Props>) {
|
||||
if (prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl ||
|
||||
prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl
|
||||
) {
|
||||
|
@ -125,7 +123,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private async checkServerLiveliness(serverConfig): Promise<void> {
|
||||
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> {
|
||||
try {
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
||||
serverConfig.hsUrl,
|
||||
|
@ -135,8 +133,15 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
serverIsAlive: true,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password") as IState);
|
||||
} catch (e: any) {
|
||||
const {
|
||||
serverIsAlive,
|
||||
serverDeadError,
|
||||
} = AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password");
|
||||
this.setState({
|
||||
serverIsAlive,
|
||||
errorText: serverDeadError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,94 +158,85 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
public submitPasswordReset(email: string, password: string, logoutDevices = true): void {
|
||||
private async onPhaseEmailInputSubmit() {
|
||||
this.phase = Phase.SendingEmail;
|
||||
|
||||
if (await this.sendVerificationMail()) {
|
||||
this.phase = Phase.EmailSent;
|
||||
return;
|
||||
}
|
||||
|
||||
this.phase = Phase.EnterEmail;
|
||||
}
|
||||
|
||||
private sendVerificationMail = async (): Promise<boolean> => {
|
||||
try {
|
||||
await this.reset.requestResetToken(this.state.email);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
this.handleError(err);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private handleError(err: any): void {
|
||||
if (err?.httpStatus === 429) {
|
||||
// 429: rate limit
|
||||
const retryAfterMs = parseInt(err?.data?.retry_after_ms, 10);
|
||||
|
||||
const errorText = isNaN(retryAfterMs)
|
||||
? _t("Too many attempts in a short time. Wait some time before trying again.")
|
||||
: _t(
|
||||
"Too many attempts in a short time. Retry after %(timeout)s.",
|
||||
{
|
||||
timeout: formatSeconds(retryAfterMs / 1000),
|
||||
},
|
||||
);
|
||||
|
||||
this.setState({
|
||||
errorText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (err?.name === "ConnectionError") {
|
||||
this.setState({
|
||||
errorText: _t("Cannot reach homeserver") + ": "
|
||||
+ _t("Ensure you have a stable internet connection, or get in touch with the server admin"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
phase: Phase.SendingEmail,
|
||||
});
|
||||
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
|
||||
this.reset.resetPassword(email, password, logoutDevices).then(() => {
|
||||
this.setState({
|
||||
phase: Phase.EmailSent,
|
||||
});
|
||||
}, (err) => {
|
||||
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||
this.setState({
|
||||
phase: Phase.Forgot,
|
||||
});
|
||||
errorText: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
private onVerify = async (ev: React.MouseEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
if (!this.reset) {
|
||||
logger.error("onVerify called before submitPasswordReset!");
|
||||
return;
|
||||
}
|
||||
if (this.state.currentHttpRequest) return;
|
||||
private async onPhaseEmailSentSubmit() {
|
||||
this.setState({
|
||||
phase: Phase.PasswordInput,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleHttpRequest(this.reset.checkEmailLinkClicked());
|
||||
this.setState({ phase: Phase.Done });
|
||||
} catch (err) {
|
||||
this.showErrorDialog(err.message);
|
||||
}
|
||||
};
|
||||
private set phase(phase: Phase) {
|
||||
this.setState({ phase });
|
||||
}
|
||||
|
||||
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
if (this.state.currentHttpRequest) return;
|
||||
|
||||
// refresh the server errors, just in case the server came back online
|
||||
await this.handleHttpRequest(this.checkServerLiveliness(this.props.serverConfig));
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.logoutDevices) {
|
||||
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
<p>{ !this.state.serverSupportsControlOfDevicesLogout ?
|
||||
_t(
|
||||
"Resetting your password on this homeserver will cause all of your devices to be " +
|
||||
"signed out. This will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
) :
|
||||
_t(
|
||||
"Signing out your devices will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
)
|
||||
}</p>
|
||||
<p>{ _t(
|
||||
"If you want to retain access to your chat history in encrypted rooms, set up Key Backup " +
|
||||
"or export your message keys from one of your other devices before proceeding.",
|
||||
) }</p>
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
this.submitPasswordReset(this.state.email, this.state.password, this.state.logoutDevices);
|
||||
};
|
||||
|
||||
private async verifyFieldsBeforeSubmit() {
|
||||
private async verifyFieldsBeforeSubmit(): Promise<boolean> {
|
||||
const fieldIdsInDisplayOrder = [
|
||||
ForgotPasswordField.Email,
|
||||
ForgotPasswordField.Password,
|
||||
ForgotPasswordField.PasswordConfirm,
|
||||
this.fieldPassword,
|
||||
this.fieldPasswordConfirm,
|
||||
];
|
||||
|
||||
const invalidFields = [];
|
||||
for (const fieldId of fieldIdsInDisplayOrder) {
|
||||
const valid = await this[fieldId].validate({ allowEmpty: false });
|
||||
const invalidFields: Field[] = [];
|
||||
|
||||
for (const field of fieldIdsInDisplayOrder) {
|
||||
if (!field) continue;
|
||||
|
||||
const valid = await field.validate({ allowEmpty: false });
|
||||
if (!valid) {
|
||||
invalidFields.push(this[fieldId]);
|
||||
invalidFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,6 +252,78 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
return false;
|
||||
}
|
||||
|
||||
private async onPhasePasswordInputSubmit(): Promise<void> {
|
||||
if (!await this.verifyFieldsBeforeSubmit()) return;
|
||||
|
||||
if (this.state.logoutDevices) {
|
||||
const logoutDevicesConfirmation = await this.renderConfirmLogoutDevicesDialog();
|
||||
if (!logoutDevicesConfirmation) return;
|
||||
}
|
||||
|
||||
this.phase = Phase.ResettingPassword;
|
||||
|
||||
try {
|
||||
await this.reset.setNewPassword(this.state.password);
|
||||
} catch (err: any) {
|
||||
if (err.httpStatus !== 401) {
|
||||
// 401 = waiting for email verification, else unknown error
|
||||
this.handleError(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const modal = Modal.createDialog(
|
||||
VerifyEmailModal,
|
||||
{
|
||||
email: this.state.email,
|
||||
errorText: this.state.errorText,
|
||||
onResendClick: this.sendVerificationMail,
|
||||
},
|
||||
"mx_VerifyEMailDialog",
|
||||
false,
|
||||
false,
|
||||
{
|
||||
// this modal cannot be dismissed except reset is done or forced
|
||||
onBeforeClose: async (reason?: string) => {
|
||||
return this.state.phase === Phase.Done || reason === "force";
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await this.reset.retrySetNewPassword(this.state.password);
|
||||
this.phase = Phase.Done;
|
||||
modal.close();
|
||||
}
|
||||
|
||||
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// Should not happen because of disabled forms, but just return if currently doing an action.
|
||||
if ([Phase.SendingEmail, Phase.ResettingPassword].includes(this.state.phase)) return;
|
||||
|
||||
this.setState({
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
// Refresh the server errors. Just in case the server came back online of went offline.
|
||||
await this.checkServerLiveliness(this.props.serverConfig);
|
||||
|
||||
// Server error
|
||||
if (!this.state.serverIsAlive) return;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case Phase.EnterEmail:
|
||||
this.onPhaseEmailInputSubmit();
|
||||
break;
|
||||
case Phase.EmailSent:
|
||||
this.onPhaseEmailSentSubmit();
|
||||
break;
|
||||
case Phase.PasswordInput:
|
||||
this.onPhasePasswordInputSubmit();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
|
||||
let value = ev.currentTarget.value;
|
||||
if (stateKey === "email") value = value.trim();
|
||||
|
@ -264,139 +332,109 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
} as any);
|
||||
};
|
||||
|
||||
private onLoginClick = (ev: React.MouseEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onLoginClick();
|
||||
};
|
||||
|
||||
public showErrorDialog(description: string, title?: string) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title,
|
||||
description,
|
||||
});
|
||||
renderEnterEmail(): JSX.Element {
|
||||
return <EnterEmail
|
||||
email={this.state.email}
|
||||
errorText={this.state.errorText}
|
||||
homeserver={this.props.serverConfig.hsName}
|
||||
loading={this.state.phase === Phase.SendingEmail}
|
||||
onInputChanged={this.onInputChanged}
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
}
|
||||
|
||||
private handleHttpRequest<T = unknown>(request: Promise<T>): Promise<T> {
|
||||
this.setState({
|
||||
currentHttpRequest: request,
|
||||
});
|
||||
return request.finally(() => {
|
||||
this.setState({
|
||||
currentHttpRequest: undefined,
|
||||
});
|
||||
async renderConfirmLogoutDevicesDialog(): Promise<boolean> {
|
||||
const { finished } = Modal.createDialog<[boolean]>(QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
<p>{ !this.state.serverSupportsControlOfDevicesLogout ?
|
||||
_t(
|
||||
"Resetting your password on this homeserver will cause all of your devices to be " +
|
||||
"signed out. This will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
) :
|
||||
_t(
|
||||
"Signing out your devices will delete the message encryption keys stored on them, " +
|
||||
"making encrypted chat history unreadable.",
|
||||
)
|
||||
}</p>
|
||||
<p>{ _t(
|
||||
"If you want to retain access to your chat history in encrypted rooms, set up Key Backup " +
|
||||
"or export your message keys from one of your other devices before proceeding.",
|
||||
) }</p>
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
renderForgot() {
|
||||
let errorText = null;
|
||||
const err = this.state.errorText;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
renderCheckEmail(): JSX.Element {
|
||||
return <CheckEmail
|
||||
email={this.state.email}
|
||||
errorText={this.state.errorText}
|
||||
onResendClick={this.sendVerificationMail}
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
}
|
||||
|
||||
let serverDeadSection;
|
||||
if (!this.state.serverIsAlive) {
|
||||
const classes = classNames({
|
||||
"mx_Login_error": true,
|
||||
"mx_Login_serverError": true,
|
||||
"mx_Login_serverErrorNonFatal": !this.state.serverErrorIsFatal,
|
||||
});
|
||||
serverDeadSection = (
|
||||
<div className={classes}>
|
||||
{ this.state.serverDeadError }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderSetPassword(): JSX.Element {
|
||||
const submitButtonChild = this.state.phase === Phase.ResettingPassword
|
||||
? <Spinner w={16} h={16} />
|
||||
: _t("Reset password");
|
||||
|
||||
return <div>
|
||||
{ errorText }
|
||||
{ serverDeadSection }
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
/>
|
||||
return <>
|
||||
<LockIcon className="mx_AuthBody_lockIcon" />
|
||||
<h1>{ _t("Reset your password") }</h1>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<EmailField
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
labelRequired={_td('The email address linked to your account must be entered.')}
|
||||
labelInvalid={_td("The email address doesn't appear to be valid.")}
|
||||
value={this.state.email}
|
||||
fieldRef={field => this[ForgotPasswordField.Email] = field}
|
||||
autoFocus={true}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<PassphraseField
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_td('New Password')}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
fieldRef={field => this[ForgotPasswordField.Password] = field}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<PassphraseConfirmField
|
||||
name="reset_password_confirm"
|
||||
label={_td('Confirm')}
|
||||
labelRequired={_td("A new password must be entered.")}
|
||||
labelInvalid={_td("New passwords must match each other.")}
|
||||
value={this.state.password2}
|
||||
password={this.state.password}
|
||||
fieldRef={field => this[ForgotPasswordField.PasswordConfirm] = field}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{ this.state.serverSupportsControlOfDevicesLogout ?
|
||||
<fieldset disabled={this.state.phase === Phase.ResettingPassword}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<StyledCheckbox onChange={() => this.setState({ logoutDevices: !this.state.logoutDevices })} checked={this.state.logoutDevices}>
|
||||
{ _t("Sign out all devices") }
|
||||
</StyledCheckbox>
|
||||
</div> : null
|
||||
}
|
||||
<span>{ _t(
|
||||
'A verification email will be sent to your inbox to confirm ' +
|
||||
'setting your new password.',
|
||||
) }</span>
|
||||
<input
|
||||
className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Send Reset Email')}
|
||||
/>
|
||||
<PassphraseField
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_td("New Password")}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
fieldRef={field => this.fieldPassword = field}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<PassphraseConfirmField
|
||||
name="reset_password_confirm"
|
||||
label={_td("Confirm new password")}
|
||||
labelRequired={_td("A new password must be entered.")}
|
||||
labelInvalid={_td("New passwords must match each other.")}
|
||||
value={this.state.password2}
|
||||
password={this.state.password}
|
||||
fieldRef={field => this.fieldPasswordConfirm = field}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{ this.state.serverSupportsControlOfDevicesLogout ?
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<StyledCheckbox onChange={() => this.setState({ logoutDevices: !this.state.logoutDevices })} checked={this.state.logoutDevices}>
|
||||
{ _t("Sign out of all devices") }
|
||||
</StyledCheckbox>
|
||||
</div> : null
|
||||
}
|
||||
{ this.state.errorText && <ErrorMessage message={this.state.errorText} /> }
|
||||
<button
|
||||
type="submit"
|
||||
className="mx_Login_submit"
|
||||
>
|
||||
{ submitButtonChild }
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<AccessibleButton kind='link' className="mx_AuthBody_changeFlow" onClick={this.onLoginClick}>
|
||||
{ _t('Sign in instead') }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderSendingEmail() {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
renderEmailSent() {
|
||||
return <div>
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
||||
"link it contains, click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input
|
||||
className="mx_Login_submit"
|
||||
type="button"
|
||||
onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
{ this.state.currentHttpRequest && (
|
||||
<div className="mx_Login_spinner"><InlineSpinner w={64} h={64} /></div>)
|
||||
}
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
|
||||
renderDone() {
|
||||
return <div>
|
||||
<p>{ _t("Your password has been reset.") }</p>
|
||||
return <>
|
||||
<CheckboxIcon className="mx_Icon mx_Icon_32 mx_Icon_accent" />
|
||||
<h1>{ _t("Your password has been reset.") }</h1>
|
||||
{ this.state.logoutDevices ?
|
||||
<p>{ _t(
|
||||
"You have been logged out of all devices and will no longer receive " +
|
||||
|
@ -410,33 +448,40 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
type="button"
|
||||
onClick={this.props.onComplete}
|
||||
value={_t('Return to login screen')} />
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let resetPasswordJsx;
|
||||
let resetPasswordJsx: JSX.Element;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case Phase.Forgot:
|
||||
resetPasswordJsx = this.renderForgot();
|
||||
break;
|
||||
case Phase.EnterEmail:
|
||||
case Phase.SendingEmail:
|
||||
resetPasswordJsx = this.renderSendingEmail();
|
||||
resetPasswordJsx = this.renderEnterEmail();
|
||||
break;
|
||||
case Phase.EmailSent:
|
||||
resetPasswordJsx = this.renderEmailSent();
|
||||
resetPasswordJsx = this.renderCheckEmail();
|
||||
break;
|
||||
case Phase.PasswordInput:
|
||||
case Phase.ResettingPassword:
|
||||
resetPasswordJsx = this.renderSetPassword();
|
||||
break;
|
||||
case Phase.Done:
|
||||
resetPasswordJsx = this.renderDone();
|
||||
break;
|
||||
default:
|
||||
resetPasswordJsx = <div className="mx_Login_spinner"><InlineSpinner w={64} h={64} /></div>;
|
||||
// This should not happen. However, it is logged and the user is sent to the start.
|
||||
logger.warn(`unknown forgot password phase ${this.state.phase}`);
|
||||
this.setState({
|
||||
phase: Phase.EnterEmail,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h1> { _t('Set a new password') } </h1>
|
||||
{ resetPasswordJsx }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue