Files
standardnotes-app-web/app/assets/javascripts/components/PasswordWizard.tsx
Mo 50c92619ce refactor: migrate remaining angular components to react (#833)
* refactor: menuRow directive to MenuRow component

* refactor: migrate footer to react

* refactor: migrate actions menu to react

* refactor: migrate history menu to react

* fix: click outside handler use capture to trigger event before re-render occurs which would otherwise cause node.contains to return incorrect result (specifically for the account menu)

* refactor: migrate revision preview modal to react

* refactor: migrate permissions modal to react

* refactor: migrate password wizard to react

* refactor: remove unused input modal directive

* refactor: remove unused delay hide component

* refactor: remove unused filechange directive

* refactor: remove unused elemReady directive

* refactor: remove unused sn-enter directive

* refactor: remove unused lowercase directive

* refactor: remove unused autofocus directive

* refactor(wip): note view to react

* refactor: use mutation observer to deinit textarea listeners

* refactor: migrate challenge modal to react

* refactor: migrate note group view to react

* refactor(wip): migrate remaining classes

* fix: navigation parent ref

* refactor: fully remove angular assets

* fix: account switcher

* fix: application view state

* refactor: remove unused password wizard type

* fix: revision preview and permissions modal

* fix: remove angular comment

* refactor: react panel resizers for editor

* feat: simple panel resizer

* fix: use simple panel resizer everywhere

* fix: simplify panel resizer state

* chore: rename simple panel resizer to panel resizer

* refactor: simplify column layout

* fix: editor mount safety check

* fix: use inline onLoad callback for iframe, as setting onload after it loads will never call it

* chore: fix note view test

* chore(deps): upgrade snjs
2022-01-30 19:01:30 -06:00

345 lines
9.8 KiB
TypeScript

import { WebApplication } from '@/ui_models/application';
import { createRef, JSX } from 'preact';
import { PureComponent } from './Abstract/PureComponent';
interface Props {
application: WebApplication;
}
type State = {
continueTitle: string;
formData: FormData;
isContinuing?: boolean;
lockContinue?: boolean;
processing?: boolean;
showSpinner?: boolean;
step: Steps;
title: string;
};
const DEFAULT_CONTINUE_TITLE = 'Continue';
enum Steps {
PasswordStep = 1,
FinishStep = 2,
}
type FormData = {
currentPassword?: string;
newPassword?: string;
newPasswordConfirmation?: string;
status?: string;
};
export class PasswordWizard extends PureComponent<Props, State> {
private currentPasswordInput = createRef<HTMLInputElement>();
constructor(props: Props) {
super(props, props.application);
this.registerWindowUnloadStopper();
this.state = {
formData: {},
continueTitle: DEFAULT_CONTINUE_TITLE,
step: Steps.PasswordStep,
title: 'Change Password',
};
}
componentDidMount(): void {
super.componentDidMount();
this.currentPasswordInput.current?.focus();
}
componentWillUnmount(): void {
super.componentWillUnmount();
window.onbeforeunload = null;
}
registerWindowUnloadStopper() {
window.onbeforeunload = () => {
return true;
};
}
resetContinueState() {
this.setState({
showSpinner: false,
continueTitle: DEFAULT_CONTINUE_TITLE,
isContinuing: false,
});
}
nextStep = async () => {
if (this.state.lockContinue || this.state.isContinuing) {
return;
}
if (this.state.step === Steps.FinishStep) {
this.dismiss();
return;
}
this.setState({
isContinuing: true,
showSpinner: true,
continueTitle: 'Generating Keys...',
});
const valid = await this.validateCurrentPassword();
if (!valid) {
this.resetContinueState();
return;
}
const success = await this.processPasswordChange();
if (!success) {
this.resetContinueState();
return;
}
this.setState({
isContinuing: false,
showSpinner: false,
continueTitle: 'Finish',
step: Steps.FinishStep,
});
};
async validateCurrentPassword() {
const currentPassword = this.state.formData.currentPassword;
const newPass = this.state.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.application.alertService.alert(
'Please enter your current password.'
);
return false;
}
if (!newPass || newPass.length === 0) {
this.application.alertService.alert('Please enter a new password.');
return false;
}
if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alertService.alert(
'Your new password does not match its confirmation.'
);
this.setFormDataState({
status: undefined,
});
return false;
}
if (!this.application.getUser()?.email) {
this.application.alertService.alert(
"We don't have your email stored. Please sign out then log back in to fix this issue."
);
this.setFormDataState({
status: undefined,
});
return false;
}
/** Validate current password */
const success = await this.application.validateAccountPassword(
this.state.formData.currentPassword!
);
if (!success) {
this.application.alertService.alert(
'The current password you entered is not correct. Please try again.'
);
}
return success;
}
async processPasswordChange() {
await this.application.downloadBackup();
this.setState({
lockContinue: true,
processing: true,
});
await this.setFormDataState({
status: 'Processing encryption keys…',
});
const newPassword = this.state.formData.newPassword;
const response = await this.application.changePassword(
this.state.formData.currentPassword!,
newPassword!
);
const success = !response.error;
this.setState({
processing: false,
lockContinue: false,
});
if (!success) {
this.setFormDataState({
status: 'Unable to process your password. Please try again.',
});
} else {
this.setState({
formData: {
...this.state.formData,
status: 'Successfully changed password.',
},
});
}
return success;
}
dismiss = () => {
if (this.state.lockContinue) {
this.application.alertService.alert(
'Cannot close window until pending tasks are complete.'
);
} else {
this.dismissModal();
}
};
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.state.formData,
...formData,
},
});
}
handleCurrentPasswordInputChange = ({
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({
currentPassword: currentTarget.value,
});
};
handleNewPasswordInputChange = ({
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({
newPassword: currentTarget.value,
});
};
handleNewPasswordConfirmationInputChange = ({
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({
newPasswordConfirmation: currentTarget.value,
});
};
render() {
return (
<div className="sn-component">
<div id="password-wizard" className="sk-modal small auto-height">
<div className="sk-modal-background" />
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-header">
<div className="sk-panel-header-title">
{this.state.title}
</div>
<a onClick={this.dismiss} className="sk-a info close-button">
Close
</a>
</div>
<div className="sk-panel-content">
{this.state.step === Steps.PasswordStep && (
<div className="sk-panel-section">
<div className="sk-panel-row">
<div className="sk-panel-column stretch">
<form className="sk-panel-form">
<label
htmlFor="password-wiz-current-password"
className="block mb-1"
>
Current Password
</label>
<input
ref={this.currentPasswordInput}
id="password-wiz-current-password"
value={this.state.formData.currentPassword}
onChange={this.handleCurrentPasswordInputChange}
type="password"
className="sk-input contrast"
/>
<div className="sk-panel-row" />
<label
htmlFor="password-wiz-new-password"
className="block mb-1"
>
New Password
</label>
<input
id="password-wiz-new-password"
value={this.state.formData.newPassword}
onChange={this.handleNewPasswordInputChange}
type="password"
className="sk-input contrast"
/>
<div className="sk-panel-row" />
<label
htmlFor="password-wiz-confirm-new-password"
className="block mb-1"
>
Confirm New Password
</label>
<input
id="password-wiz-confirm-new-password"
value={
this.state.formData.newPasswordConfirmation
}
onChange={
this.handleNewPasswordConfirmationInputChange
}
type="password"
className="sk-input contrast"
/>
</form>
</div>
</div>
</div>
)}
{this.state.step === Steps.FinishStep && (
<div className="sk-panel-section">
<div className="sk-label sk-bold info">
Your password has been successfully changed.
</div>
<p className="sk-p">
Please ensure you are running the latest version of
Standard Notes on all platforms to ensure maximum
compatibility.
</p>
</div>
)}
</div>
<div className="sk-panel-footer">
<button
onClick={this.nextStep}
disabled={this.state.lockContinue}
className="sn-button min-w-20 info"
>
{this.state.continueTitle}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}