Add ability to change password (#1532)

* Got password reset form mostly done

* Hooked up the rest of the password-change code.

* Added a completion state

* Fix weird comment that got updated by accident
This commit is contained in:
Gregory Schier 2019-05-29 16:38:16 -04:00 committed by GitHub
parent 13292448c9
commit 68ae6934cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 254 additions and 23 deletions

View File

@ -31,7 +31,7 @@ export async function login(rawEmail, rawPassphrase) {
// Fetch Salt and Submit A To Server //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
const { saltKey, saltAuth } = await fetch.post('/auth/login-s', { email }, null);
const { saltKey, saltAuth } = await _getAuthSalts(email);
const authSecret = await crypt.deriveKey(passphrase, email, saltKey);
const secret1 = await crypt.srpGenKey();
const c = new srp.Client(
@ -108,6 +108,49 @@ export async function login(rawEmail, rawPassphrase) {
_callCallbacks();
}
export async function changePasswordWithToken(rawNewPassphrase, confirmationCode) {
// Sanitize inputs
const newPassphrase = _sanitizePassphrase(rawNewPassphrase);
const newEmail = getEmail(); // Use the same one
// Fetch some things
const { saltEnc, encSymmetricKey } = await _whoami();
const { saltKey, saltAuth } = await _getAuthSalts(newEmail);
// Generate some secrets for the user base'd on password
const newSecret = await crypt.deriveKey(newPassphrase, newEmail, saltEnc);
const newAuthSecret = await crypt.deriveKey(newPassphrase, newEmail, saltKey);
const newVerifier = srp
.computeVerifier(
_getSrpParams(),
Buffer.from(saltAuth, 'hex'),
Buffer.from(newEmail, 'utf8'),
Buffer.from(newAuthSecret, 'hex'),
)
.toString('hex');
// Re-encrypt existing keys with new secret
const newEncSymmetricKeyJSON = crypt.encryptAES(newSecret, _getSymmetricKey());
const newEncSymmetricKey = JSON.stringify(newEncSymmetricKeyJSON);
return fetch.post(
`/auth/change-password`,
{
code: confirmationCode,
newEmail: newEmail,
encSymmetricKey: encSymmetricKey,
newVerifier,
newEncSymmetricKey,
},
getCurrentSessionId(),
);
}
export function sendPasswordChangeCode() {
return fetch.post('/auth/send-password-code', null, getCurrentSessionId());
}
export function getPublicKey() {
return _getSessionData().publicKey;
}
@ -138,6 +181,14 @@ export function getFirstName() {
return _getSessionData().firstName;
}
export function getLastName() {
return _getSessionData().firstName;
}
export function getFullName() {
return `${getFirstName()} ${getLastName()}`.trim();
}
/** Check if we (think) we have a session */
export function isLoggedIn() {
return !!getCurrentSessionId();
@ -197,10 +248,19 @@ export async function endTrial() {
// Helper Functions //
// ~~~~~~~~~~~~~~~~ //
function _getSymmetricKey() {
const sessionData = _getSessionData();
return sessionData.symmetricKey;
}
function _whoami(sessionId = null) {
return fetch.get('/auth/whoami', sessionId || getCurrentSessionId());
}
function _getAuthSalts(email) {
return fetch.post('/auth/login-s', { email }, getCurrentSessionId());
}
function _getSessionData() {
const sessionId = getCurrentSessionId();
if (!sessionId || !window) {

View File

@ -10,6 +10,7 @@ type Props = {|
onClick?: Function,
className?: string,
children?: React.Node,
disabled?: boolean,
|};
@autobind
@ -31,6 +32,7 @@ class Link extends React.PureComponent<Props> {
href,
children,
className,
disabled,
...other
} = this.props;
return button ? (
@ -42,6 +44,7 @@ class Link extends React.PureComponent<Props> {
href={href}
onClick={this._handleClick}
className={(className || '') + ' theme--link'}
disabled={disabled}
{...other}>
{children}
</a>

View File

@ -82,7 +82,7 @@ class SettingsModal extends PureComponent {
render() {
const { settings } = this.props;
const { currentTabIndex } = this.state;
const email = session.isLoggedIn() ? session.getEmail() : null;
const email = session.isLoggedIn() ? session.getFullName() : null;
return (
<Modal ref={this._setModalRef} tall freshState {...this.props}>

View File

@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import * as sync from '../../../sync-legacy/index';
import Link from '../base/link';
@ -6,23 +7,109 @@ import LoginModal from '../modals/login-modal';
import { hideAllModals, showModal } from '../modals/index';
import PromptButton from '../base/prompt-button';
import * as session from '../../../account/session';
import HelpTooltip from '../help-tooltip';
type Props = {};
type State = {
code: string,
password: string,
password2: string,
showChangePassword: boolean,
codeSent: boolean,
error: string,
finishedResetting: boolean,
};
@autobind
class Account extends PureComponent {
class Account extends React.PureComponent<Props, State> {
state = {
code: '',
password: '',
password2: '',
codeSent: false,
showChangePassword: false,
error: '',
finishedResetting: false,
};
async _handleShowChangePasswordForm(e: SyntheticEvent<HTMLInputElement>) {
this.setState(state => ({
showChangePassword: !state.showChangePassword,
finishedResetting: false,
}));
}
_handleChangeCode(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ code: e.currentTarget.value });
}
_handleChangePassword(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ password: e.currentTarget.value });
}
_handleChangePassword2(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ password2: e.currentTarget.value });
}
async _handleSubmitPasswordChange(e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
this.setState({ error: '' });
const { password, password2, code } = this.state;
let error = '';
if (password !== password2) {
error = 'Passwords did not match';
} else if (!code) {
error = 'Code was not provided';
}
if (error) {
this.setState({ error });
return;
}
try {
await session.changePasswordWithToken(password, code);
} catch (err) {
this.setState({ error: err.message });
return;
}
this.setState({ error: '', finishedResetting: true, showChangePassword: false });
}
async _handleLogout() {
await sync.logout();
this.forceUpdate();
}
_handleLogin(e) {
static _handleLogin(e: SyntheticEvent<HTMLButtonElement>) {
e.preventDefault();
hideAllModals();
showModal(LoginModal);
}
renderUpgrade() {
async _sendCode() {
try {
await session.sendPasswordChangeCode();
} catch (err) {
this.setState({ error: err.message });
return;
}
this.setState({ codeSent: true });
}
async _handleSendCode(e: SyntheticEvent<HTMLAnchorElement>) {
e.preventDefault();
await this._sendCode();
}
static renderUpgrade() {
return (
<div>
<React.Fragment>
<div className="notice pad surprise">
<h1 className="no-margin-top">Try Insomnia Plus!</h1>
<p>
@ -48,34 +135,111 @@ class Account extends PureComponent {
</div>
<p>
Or{' '}
<a href="#" onClick={this._handleLogin} className="theme--link">
<a href="#" onClick={Account._handleLogin} className="theme--link">
Login
</a>
</p>
</div>
</React.Fragment>
);
}
renderAccount() {
const {
code,
password,
password2,
codeSent,
showChangePassword,
error,
finishedResetting,
} = this.state;
return (
<div>
<h2 className="no-margin-top">Welcome {session.getFirstName()}!</h2>
<p>
You are currently logged in as <code className="code--compact">{session.getEmail()}</code>
</p>
<br />
<Link button href="https://insomnia.rest/app/" className="btn btn--clicky">
Manage Account
</Link>
<PromptButton className="margin-left-sm btn btn--clicky" onClick={this._handleLogout}>
Sign Out
</PromptButton>
</div>
<React.Fragment>
<div>
<h2 className="no-margin-top">Welcome {session.getFirstName()}!</h2>
<p>
You are currently logged in as{' '}
<code className="code--compact">{session.getEmail()}</code>
</p>
<br />
<Link button href="https://insomnia.rest/app/" className="btn btn--clicky">
Manage Account
</Link>
<PromptButton className="space-left btn btn--clicky" onClick={this._handleLogout}>
Sign Out
</PromptButton>
<button
className="space-left btn btn--clicky"
onClick={this._handleShowChangePasswordForm}>
Change Password
</button>
</div>
{finishedResetting && (
<p className="notice surprise">Your password was changed successfully</p>
)}
{showChangePassword && (
<form onSubmit={this._handleSubmitPasswordChange} className="pad-top">
<hr />
{error && <p className="notice error">{error}</p>}
<div className="form-control form-control--outlined">
<label>
New Password
<input
type="password"
placeholder="•••••••••••••••••"
onChange={this._handleChangePassword}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
Confirm Password
<input
type="password"
placeholder="•••••••••••••••••"
onChange={this._handleChangePassword2}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
Confirmation Code{' '}
<HelpTooltip>A confirmation code has been sent to your email address</HelpTooltip>
<input
type="text"
defaultValue={code}
placeholder="aa8b0d1ea9"
onChange={this._handleChangeCode}
/>
</label>
</div>
<div className="row-spaced row--top">
<div>
{codeSent ? 'A code was sent to your email' : 'Looking for a code?'}{' '}
<Link href="#" onClick={this._handleSendCode}>
Email Me a Code
</Link>
</div>
<div className="text-right">
<button
type="submit"
className="btn btn--clicky"
disabled={!code || !password || password !== password2}>
Submit Change
</button>
</div>
</div>
</form>
)}
</React.Fragment>
);
}
render() {
return session.isLoggedIn() ? this.renderAccount() : this.renderUpgrade();
return session.isLoggedIn() ? this.renderAccount() : Account.renderUpgrade();
}
}

View File

@ -410,6 +410,10 @@ blockquote {
width: 100%;
}
&.row--top {
align-items: start;
}
.valign-middle {
vertical-align: middle;
}