diff --git a/packages/insomnia-app/app/account/session.js b/packages/insomnia-app/app/account/session.js index 124ebcd3d..918da9255 100644 --- a/packages/insomnia-app/app/account/session.js +++ b/packages/insomnia-app/app/account/session.js @@ -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) { diff --git a/packages/insomnia-app/app/ui/components/base/link.js b/packages/insomnia-app/app/ui/components/base/link.js index 2817e337d..a41d44f0b 100644 --- a/packages/insomnia-app/app/ui/components/base/link.js +++ b/packages/insomnia-app/app/ui/components/base/link.js @@ -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 { href, children, className, + disabled, ...other } = this.props; return button ? ( @@ -42,6 +44,7 @@ class Link extends React.PureComponent { href={href} onClick={this._handleClick} className={(className || '') + ' theme--link'} + disabled={disabled} {...other}> {children} diff --git a/packages/insomnia-app/app/ui/components/modals/settings-modal.js b/packages/insomnia-app/app/ui/components/modals/settings-modal.js index 99bced646..80541131f 100644 --- a/packages/insomnia-app/app/ui/components/modals/settings-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/settings-modal.js @@ -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 ( diff --git a/packages/insomnia-app/app/ui/components/settings/account.js b/packages/insomnia-app/app/ui/components/settings/account.js index adf0d3fce..88e852e36 100644 --- a/packages/insomnia-app/app/ui/components/settings/account.js +++ b/packages/insomnia-app/app/ui/components/settings/account.js @@ -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 { + state = { + code: '', + password: '', + password2: '', + codeSent: false, + showChangePassword: false, + error: '', + finishedResetting: false, + }; + + async _handleShowChangePasswordForm(e: SyntheticEvent) { + this.setState(state => ({ + showChangePassword: !state.showChangePassword, + finishedResetting: false, + })); + } + + _handleChangeCode(e: SyntheticEvent) { + this.setState({ code: e.currentTarget.value }); + } + + _handleChangePassword(e: SyntheticEvent) { + this.setState({ password: e.currentTarget.value }); + } + + _handleChangePassword2(e: SyntheticEvent) { + this.setState({ password2: e.currentTarget.value }); + } + + async _handleSubmitPasswordChange(e: SyntheticEvent) { + 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) { 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) { + e.preventDefault(); + await this._sendCode(); + } + + static renderUpgrade() { return ( -
+

Try Insomnia Plus!

@@ -48,34 +135,111 @@ class Account extends PureComponent {

Or{' '} - + Login

-
+ ); } renderAccount() { + const { + code, + password, + password2, + codeSent, + showChangePassword, + error, + finishedResetting, + } = this.state; + return ( -
-

Welcome {session.getFirstName()}!

-

- You are currently logged in as {session.getEmail()} -

-
- - Manage Account - - - Sign Out - -
+ +
+

Welcome {session.getFirstName()}!

+

+ You are currently logged in as{' '} + {session.getEmail()} +

+
+ + Manage Account + + + Sign Out + + +
+ + {finishedResetting && ( +

Your password was changed successfully

+ )} + + {showChangePassword && ( +
+
+ {error &&

{error}

} +
+ +
+
+ +
+
+ +
+
+
+ {codeSent ? 'A code was sent to your email' : 'Looking for a code?'}{' '} + + Email Me a Code + +
+
+ +
+
+
+ )} +
); } render() { - return session.isLoggedIn() ? this.renderAccount() : this.renderUpgrade(); + return session.isLoggedIn() ? this.renderAccount() : Account.renderUpgrade(); } } diff --git a/packages/insomnia-app/app/ui/css/layout/base.less b/packages/insomnia-app/app/ui/css/layout/base.less index 940fd4bea..e43077fc0 100644 --- a/packages/insomnia-app/app/ui/css/layout/base.less +++ b/packages/insomnia-app/app/ui/css/layout/base.less @@ -410,6 +410,10 @@ blockquote { width: 100%; } +&.row--top { + align-items: start; +} + .valign-middle { vertical-align: middle; }