334 lines
9.7 KiB
TypeScript
334 lines
9.7 KiB
TypeScript
import { WebApplication } from '@/UIModels/Application'
|
|
import { createRef, JSX } from 'preact'
|
|
import { PureComponent } from '@/Components/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',
|
|
}
|
|
}
|
|
|
|
override componentDidMount(): void {
|
|
super.componentDidMount()
|
|
this.currentPasswordInput.current?.focus()
|
|
}
|
|
|
|
override 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.')
|
|
.catch(console.error)
|
|
return false
|
|
}
|
|
|
|
if (!newPass || newPass.length === 0) {
|
|
this.application.alertService.alert('Please enter a new password.').catch(console.error)
|
|
return false
|
|
}
|
|
if (newPass !== this.state.formData.newPasswordConfirmation) {
|
|
this.application.alertService
|
|
.alert('Your new password does not match its confirmation.')
|
|
.catch(console.error)
|
|
this.setFormDataState({
|
|
status: undefined,
|
|
}).catch(console.error)
|
|
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.",
|
|
)
|
|
.catch(console.error)
|
|
this.setFormDataState({
|
|
status: undefined,
|
|
}).catch(console.error)
|
|
return false
|
|
}
|
|
|
|
/** Validate current password */
|
|
const success = await this.application.validateAccountPassword(
|
|
this.state.formData.currentPassword as string,
|
|
)
|
|
if (!success) {
|
|
this.application.alertService
|
|
.alert('The current password you entered is not correct. Please try again.')
|
|
.catch(console.error)
|
|
}
|
|
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 as string,
|
|
newPassword as string,
|
|
)
|
|
|
|
const success = !response.error
|
|
this.setState({
|
|
processing: false,
|
|
lockContinue: false,
|
|
})
|
|
|
|
if (!success) {
|
|
this.setFormDataState({
|
|
status: 'Unable to process your password. Please try again.',
|
|
}).catch(console.error)
|
|
} 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.')
|
|
.catch(console.error)
|
|
} 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,
|
|
}).catch(console.error)
|
|
}
|
|
|
|
handleNewPasswordInputChange = ({
|
|
currentTarget,
|
|
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
|
this.setFormDataState({
|
|
newPassword: currentTarget.value,
|
|
}).catch(console.error)
|
|
}
|
|
|
|
handleNewPasswordConfirmationInputChange = ({
|
|
currentTarget,
|
|
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
|
this.setFormDataState({
|
|
newPasswordConfirmation: currentTarget.value,
|
|
}).catch(console.error)
|
|
}
|
|
|
|
override 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>
|
|
)
|
|
}
|
|
}
|