From f127a9712cf0ddd9656eac554b834c49dca41657 Mon Sep 17 00:00:00 2001 From: Oskar Hane Date: Mon, 28 Oct 2019 16:42:23 +0100 Subject: [PATCH 1/8] Retry reconnection on pw-change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the new password successfully was set when changing password, but it’s not accepted when trying to connect, retry up to 5 times before failing. Also, when validating old credentials, don’t reconstruct the main driver instance. Create a new one and validate on that one. --- .../modules/Stream/Auth/ConnectionForm.jsx | 56 ++++++++++++++----- .../modules/connections/connectionsDuck.js | 29 +++++++++- src/shared/rootEpic.js | 4 +- src/shared/services/bolt/boltConnection.js | 2 +- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/src/browser/modules/Stream/Auth/ConnectionForm.jsx b/src/browser/modules/Stream/Auth/ConnectionForm.jsx index 66d03531848..1af188bf260 100644 --- a/src/browser/modules/Stream/Auth/ConnectionForm.jsx +++ b/src/browser/modules/Stream/Auth/ConnectionForm.jsx @@ -26,7 +26,8 @@ import { getActiveConnection, setActiveConnection, updateConnection, - CONNECT + CONNECT, + VERIFY_CREDENTIALS } from 'shared/modules/connections/connectionsDuck' import { getInitCmd } from 'shared/modules/settings/settingsDuck' import { executeSystemCommand } from 'shared/modules/commands/commandsDuck' @@ -57,24 +58,37 @@ export class ConnectionForm extends Component { } tryConnect = (password, doneFn) => { this.props.error({}) - this.props.bus.self(CONNECT, { ...this.state, password }, res => { - doneFn(res) - }) + this.props.bus.self( + VERIFY_CREDENTIALS, + { ...this.state, password }, + res => { + doneFn(res) + } + ) } - connect = (doneFn = () => {}) => { + connect = (doneFn = () => {}, onError = null) => { this.props.error({}) - this.props.bus.self(CONNECT, this.state, res => { - doneFn() - if (res.success) { - this.saveAndStart() - } else { - if (res.error.code === 'Neo.ClientError.Security.CredentialsExpired') { - this.setState({ passwordChangeNeeded: true }) + this.props.bus.self( + CONNECT, + { ...this.state, noResetConnectionOnFail: true }, + res => { + doneFn() + if (res.success) { + this.saveAndStart() } else { - this.props.error(res.error) + if ( + res.error.code === 'Neo.ClientError.Security.CredentialsExpired' + ) { + this.setState({ passwordChangeNeeded: true }) + } else { + if (onError) { + return onError(res) + } + this.props.error(res.error) + } } } - }) + ) } onUsernameChange (event) { const username = event.target.value @@ -117,7 +131,19 @@ export class ConnectionForm extends Component { response => { if (response.success) { return this.setState({ password: newPassword }, () => { - this.connect() + let retries = 5 + const retryFn = res => { + // New password not accepted yet, initiate retry + if (res.error.code === 'Neo.ClientError.Security.Unauthorized') { + retries-- + if (retries > 0) { + this.connect(undefined, retryFn) + } + } else { + this.props.error(res.error) + } + } + this.connect(undefined, retryFn) }) } this.props.error(response.error) diff --git a/src/shared/modules/connections/connectionsDuck.js b/src/shared/modules/connections/connectionsDuck.js index d07ef5705ca..02ddd17d0b0 100644 --- a/src/shared/modules/connections/connectionsDuck.js +++ b/src/shared/modules/connections/connectionsDuck.js @@ -56,6 +56,7 @@ export const UPDATE_AUTH_ENABLED = NAME + '/UPDATE_AUTH_ENABLED' export const SWITCH_CONNECTION = NAME + '/SWITCH_CONNECTION' export const SWITCH_CONNECTION_SUCCESS = NAME + '/SWITCH_CONNECTION_SUCCESS' export const SWITCH_CONNECTION_FAILED = NAME + '/SWITCH_CONNECTION_FAILED' +export const VERIFY_CREDENTIALS = NAME + '/VERIFY_CREDENTIALS' export const DISCONNECTED_STATE = 0 export const CONNECTED_STATE = 1 @@ -303,10 +304,12 @@ export const setAuthEnabled = authEnabled => { // Epics export const connectEpic = (action$, store) => { - return action$.ofType(CONNECT).mergeMap(action => { + return action$.ofType(CONNECT).mergeMap(async action => { if (!action.$$responseChannel) return Rx.Observable.of(null) memoryUsername = '' memoryPassword = '' + bolt.closeConnection() + await new Promise(resolve => setTimeout(() => resolve()), 2000) return bolt .openConnection(action, { encrypted: getEncryptionMode(action), @@ -314,8 +317,9 @@ export const connectEpic = (action$, store) => { }) .then(res => ({ type: action.$$responseChannel, success: true })) .catch(e => { - store.dispatch(setActiveConnection(null)) - + if (!action.noResetConnectionOnFail) { + store.dispatch(setActiveConnection(null)) + } return { type: action.$$responseChannel, success: false, @@ -324,6 +328,25 @@ export const connectEpic = (action$, store) => { }) }) } + +export const verifyConnectionCredentialsEpic = (action$, store) => { + return action$.ofType(VERIFY_CREDENTIALS).mergeMap(action => { + if (!action.$$responseChannel) return Rx.Observable.of(null) + return bolt + .directConnect( + action, + { encrypted: getEncryptionMode(action) }, + undefined + ) + .then(driver => { + return { type: action.$$responseChannel, success: true } + }) + .catch(e => { + return { type: action.$$responseChannel, success: false, error: e } + }) + }) +} + export const startupConnectEpic = (action$, store) => { return action$.ofType(discovery.DONE).mergeMap(action => { const connection = getConnection(store.getState(), discovery.CONNECTION_ID) diff --git a/src/shared/rootEpic.js b/src/shared/rootEpic.js index 98d0680e84d..65559b955b5 100644 --- a/src/shared/rootEpic.js +++ b/src/shared/rootEpic.js @@ -38,7 +38,8 @@ import { switchConnectionEpic, switchConnectionSuccessEpic, switchConnectionFailEpic, - silentDisconnectEpic + silentDisconnectEpic, + verifyConnectionCredentialsEpic } from './modules/connections/connectionsDuck' import { dbMetaEpic, @@ -99,6 +100,7 @@ export default combineEpics( jmxEpic, disconnectEpic, silentDisconnectEpic, + verifyConnectionCredentialsEpic, startupConnectEpic, disconnectSuccessEpic, startupConnectionSuccessEpic, diff --git a/src/shared/services/bolt/boltConnection.js b/src/shared/services/bolt/boltConnection.js index e2678dc95ca..ebf569a968d 100644 --- a/src/shared/services/bolt/boltConnection.js +++ b/src/shared/services/bolt/boltConnection.js @@ -283,7 +283,7 @@ export const closeConnection = () => { } export const ensureConnection = (props, opts, onLostConnection) => { - const session = _drivers ? _drivers.getDirectDriver().session() : false + const session = _drivers ? _drivers.getRoutedDriver().session() : false if (session) { return new Promise((resolve, reject) => { session.close() From c731cf654e51b553dc84ee6ae3df5bdfe352f8c4 Mon Sep 17 00:00:00 2001 From: Oskar Hane Date: Mon, 28 Oct 2019 19:24:31 +0100 Subject: [PATCH 2/8] Add 200ms delay between connection retries --- src/browser/modules/Stream/Auth/ConnectionForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/modules/Stream/Auth/ConnectionForm.jsx b/src/browser/modules/Stream/Auth/ConnectionForm.jsx index 1af188bf260..45f1a6eaf5a 100644 --- a/src/browser/modules/Stream/Auth/ConnectionForm.jsx +++ b/src/browser/modules/Stream/Auth/ConnectionForm.jsx @@ -137,7 +137,7 @@ export class ConnectionForm extends Component { if (res.error.code === 'Neo.ClientError.Security.Unauthorized') { retries-- if (retries > 0) { - this.connect(undefined, retryFn) + setTimeout(() => this.connect(undefined, retryFn), 200) } } else { this.props.error(res.error) From 41bef864c95de1aa7b87ce5ee71d8da6ad936f0d Mon Sep 17 00:00:00 2001 From: Linus Lundahl Date: Fri, 12 Jul 2019 15:50:58 +0200 Subject: [PATCH 3/8] Update change password frame. * Add isLoading state. * Don't show the form if there's no active connection. --- .../Stream/Auth/ChangePasswordForm.jsx | 30 ++++++++++---- .../Stream/Auth/ChangePasswordFrame.jsx | 41 +++++++++++++------ .../modules/Stream/Auth/ConnectionForm.jsx | 20 +++++++-- src/browser/modules/Stream/Auth/styled.jsx | 4 ++ 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/src/browser/modules/Stream/Auth/ChangePasswordForm.jsx b/src/browser/modules/Stream/Auth/ChangePasswordForm.jsx index bb9626fe5b9..94689a26dc8 100644 --- a/src/browser/modules/Stream/Auth/ChangePasswordForm.jsx +++ b/src/browser/modules/Stream/Auth/ChangePasswordForm.jsx @@ -85,8 +85,13 @@ export default class ChangePasswordForm extends Component { } render () { const indexStart = this.props.showExistingPasswordInput ? 1 : 0 + const { isLoading } = this.props + const classNames = [] + if (isLoading) { + classNames.push('isLoading') + } return ( - + setRefForIndex(0, ref) + ref: ref => setRefForIndex(0, ref), + disabled: isLoading })} /> @@ -122,7 +128,8 @@ export default class ChangePasswordForm extends Component { type: 'password', onChange: this.onNewPasswordChange, value: this.state.newPassword, - ref: ref => setRefForIndex(indexStart, ref) + ref: ref => setRefForIndex(indexStart, ref), + disabled: isLoading })} /> @@ -136,15 +143,20 @@ export default class ChangePasswordForm extends Component { type: 'password', onChange: this.onNewPasswordChange2, value: this.state.newPassword2, - ref: ref => setRefForIndex(indexStart + 1, ref) + ref: ref => setRefForIndex(indexStart + 1, ref), + disabled: isLoading })} /> - + + + + Please wait... ) }} diff --git a/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx b/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx index 2ef9e75d9b4..d3ce5292d0c 100644 --- a/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx +++ b/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx @@ -19,6 +19,7 @@ */ import React, { Component } from 'react' +import { connect } from 'react-redux' import ConnectionForm from './ConnectionForm' import FrameTemplate from '../../Frame/FrameTemplate' @@ -26,7 +27,8 @@ import FrameError from '../../Frame/FrameError' import Render from 'browser-components/Render' import { H3 } from 'browser-components/headers' import { Lead } from 'browser-components/Text' -import { StyledConnectionAside } from './styled' +import { StyledConnectionFrame, StyledConnectionAside } from './styled' +import { getActiveConnection } from 'shared/modules/connections/connectionsDuck' export class ChangePasswordFrame extends Component { constructor (props) { @@ -51,13 +53,14 @@ export class ChangePasswordFrame extends Component { } render () { const content = ( - +

Password change

- Enter your current password and the new twice to change your - password. + {this.props.activeConnection + ? 'Enter your current password and the new twice to change your password.' + : 'Please connect to a database to change the password.'} @@ -65,14 +68,16 @@ export class ChangePasswordFrame extends Component {
- -
+ + + + ) return ( { + return { + activeConnection: getActiveConnection(state) + } +} + +export default connect( + mapStateToProps, + () => ({}) +)(ChangePasswordFrame) diff --git a/src/browser/modules/Stream/Auth/ConnectionForm.jsx b/src/browser/modules/Stream/Auth/ConnectionForm.jsx index 45f1a6eaf5a..aa102f8aae9 100644 --- a/src/browser/modules/Stream/Auth/ConnectionForm.jsx +++ b/src/browser/modules/Stream/Auth/ConnectionForm.jsx @@ -50,6 +50,7 @@ export class ConnectionForm extends Component { this.state = { ...connection, isConnected: isConnected, + isLoading: false, passwordChangeNeeded: props.passwordChangeNeeded || false, forcePasswordChange: props.forcePasswordChange || false, successCallback: props.onSuccess || (() => {}), @@ -113,9 +114,11 @@ export class ConnectionForm extends Component { } onChangePassword ({ newPassword, error }) { if (error && error.code) { + this.setState({ isLoading: false }) return this.props.error(error) } if (this.state.password === null) { + this.setState({ isLoading: false }) return this.props.error({ message: 'Please set existing password' }) } this.props.error({}) @@ -137,14 +140,20 @@ export class ConnectionForm extends Component { if (res.error.code === 'Neo.ClientError.Security.Unauthorized') { retries-- if (retries > 0) { - setTimeout(() => this.connect(undefined, retryFn), 200) + setTimeout(() => this.connect(() => { + this.setState({ isLoading: false }) + }, retryFn), 200) } } else { this.props.error(res.error) } } - this.connect(undefined, retryFn) + this.connect(() => { + this.setState({ isLoading: false }) + }, retryFn) }) + } else { + this.setState({ isLoading: false }) } this.props.error(response.error) } @@ -186,7 +195,12 @@ export class ConnectionForm extends Component { showExistingPasswordInput={this.props.showExistingPasswordInput} onChangePasswordClick={this.onChangePassword.bind(this)} onChange={this.onChangePasswordChange.bind(this)} - tryConnect={this.tryConnect} + tryConnect={(password, doneFn) => { + this.setState({ isLoading: true }, () => + this.tryConnect(password, doneFn) + ) + }} + isLoading={this.state.isLoading} > {this.props.children} diff --git a/src/browser/modules/Stream/Auth/styled.jsx b/src/browser/modules/Stream/Auth/styled.jsx index 9873d2811df..9b9b858c53f 100644 --- a/src/browser/modules/Stream/Auth/styled.jsx +++ b/src/browser/modules/Stream/Auth/styled.jsx @@ -24,6 +24,10 @@ import { StyledFrameAside } from '../../Frame/styled' export const StyledConnectionForm = styled.form` padding: 0 15px; + + &.isLoading { + opacity: 0.5; + } ` export const StyledConnectionAside = styled(StyledFrameAside)`` export const StyledConnectionFormEntry = styled.div` From e3abef1b4ff499c92ae0544d69177cbdb2ad84ca Mon Sep 17 00:00:00 2001 From: Linus Lundahl Date: Tue, 29 Oct 2019 14:56:34 +0100 Subject: [PATCH 4/8] Fix success color in change password frame. --- src/browser/modules/Frame/FrameSuccess.jsx | 2 +- src/browser/modules/Frame/styled.jsx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browser/modules/Frame/FrameSuccess.jsx b/src/browser/modules/Frame/FrameSuccess.jsx index 8c99fb6d62b..90e0a5614fe 100644 --- a/src/browser/modules/Frame/FrameSuccess.jsx +++ b/src/browser/modules/Frame/FrameSuccess.jsx @@ -20,7 +20,7 @@ import React from 'react' const FrameSuccess = props => { if (!props || !props.message) return null - return {props.message} + return {props.message} } export default FrameSuccess diff --git a/src/browser/modules/Frame/styled.jsx b/src/browser/modules/Frame/styled.jsx index f419a31d4a1..f26f1613b2f 100644 --- a/src/browser/modules/Frame/styled.jsx +++ b/src/browser/modules/Frame/styled.jsx @@ -126,6 +126,12 @@ export const StyledFrameStatusbar = styled.div` display: flex; flex-direction: row; flex: none; + align-items: center; + padding-left: 10px; + + .statusbar--success { + color: ${props => props.theme.success}; + } ` export const StyledFrameSidebar = styled.ul` From a6592a0d2f1cd489e3089d9b3ea937f777107a4b Mon Sep 17 00:00:00 2001 From: Linus Lundahl Date: Tue, 29 Oct 2019 14:57:14 +0100 Subject: [PATCH 5/8] Fix forced password change behaviour. --- src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx | 6 +++--- src/browser/modules/Stream/Auth/ConnectionForm.jsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx b/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx index d3ce5292d0c..0b39281ffe6 100644 --- a/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx +++ b/src/browser/modules/Stream/Auth/ChangePasswordFrame.jsx @@ -27,7 +27,7 @@ import FrameError from '../../Frame/FrameError' import Render from 'browser-components/Render' import { H3 } from 'browser-components/headers' import { Lead } from 'browser-components/Text' -import { StyledConnectionFrame, StyledConnectionAside } from './styled' +import { StyledConnectionAside } from './styled' import { getActiveConnection } from 'shared/modules/connections/connectionsDuck' export class ChangePasswordFrame extends Component { @@ -53,7 +53,7 @@ export class ChangePasswordFrame extends Component { } render () { const content = ( - +

Password change

@@ -77,7 +77,7 @@ export class ChangePasswordFrame extends Component { showExistingPasswordInput /> -
+ ) return ( Date: Tue, 29 Oct 2019 14:57:28 +0100 Subject: [PATCH 6/8] Reset user add form fields on success. --- src/browser/modules/User/UserAdd.jsx | 38 +++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/browser/modules/User/UserAdd.jsx b/src/browser/modules/User/UserAdd.jsx index 32a458bd8e2..46e2e754f2e 100644 --- a/src/browser/modules/User/UserAdd.jsx +++ b/src/browser/modules/User/UserAdd.jsx @@ -59,9 +59,10 @@ export class UserAdd extends Component { username: '', password: '', confirmPassword: '', - forcePasswordChange: false, + forcePasswordChange: '', errors: null, - success: null + success: null, + isLoading: false } } componentWillMount () { @@ -116,9 +117,17 @@ export class UserAdd extends Component { ) }) if (errors.length > 0) { - return this.setState({ errors: errors }) + return this.setState({ errors: errors, isLoading: false }) } - return this.setState({ success: `User '${this.state.username}' created` }) + return this.setState({ + success: `User '${this.state.username}' created`, + username: '', + password: '', + confirmPassword: '', + roles: [], + forcePasswordChange: '', + isLoading: false + }) } getRoles () { this.props.bus && @@ -149,7 +158,7 @@ export class UserAdd extends Component { ) } submit () { - this.setState({ success: null, errors: null }) + this.setState({ isLoading: true, success: null, errors: null }) let errors = [] if (!this.state.username) errors.push('Missing username') if (!this.state.password) errors.push('Missing password') @@ -178,7 +187,8 @@ export class UserAdd extends Component { ? response.error.message : 'Unknown error' return this.setState({ - errors: ['Unable to create user', error] + errors: ['Unable to create user', error], + isLoading: false }) } return this.addRoles() @@ -211,6 +221,8 @@ export class UserAdd extends Component { } render () { + const { isLoading } = this.props + const listOfAvailableRoles = rolesSelectorId => this.state.availableRoles ? ( @@ -256,7 +270,9 @@ export class UserAdd extends Component { className='password' name={passwordId} id={passwordId} + value={this.state.password} onChange={this.updatePassword.bind(this)} + disabled={isLoading} /> @@ -268,7 +284,9 @@ export class UserAdd extends Component { className='password-confirm' name={passwordConfirmId} id={passwordConfirmId} + value={this.state.confirmPassword} onChange={this.confirmUpdatePassword.bind(this)} + disabled={isLoading} /> @@ -283,6 +301,8 @@ export class UserAdd extends Component { Force password change @@ -290,7 +310,11 @@ export class UserAdd extends Component { - + From aa074c69f9de0978801d93b1ab85e5a8ca2c938b Mon Sep 17 00:00:00 2001 From: Oskar Hane Date: Tue, 29 Oct 2019 15:17:55 +0100 Subject: [PATCH 7/8] Restore driver of choice when ensuring connection --- src/shared/services/bolt/boltConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/services/bolt/boltConnection.js b/src/shared/services/bolt/boltConnection.js index ebf569a968d..e2678dc95ca 100644 --- a/src/shared/services/bolt/boltConnection.js +++ b/src/shared/services/bolt/boltConnection.js @@ -283,7 +283,7 @@ export const closeConnection = () => { } export const ensureConnection = (props, opts, onLostConnection) => { - const session = _drivers ? _drivers.getRoutedDriver().session() : false + const session = _drivers ? _drivers.getDirectDriver().session() : false if (session) { return new Promise((resolve, reject) => { session.close() From 7f8c59d9a99d077c81221a0595e11da29be1963f Mon Sep 17 00:00:00 2001 From: Oskar Hane Date: Fri, 8 Nov 2019 10:19:56 +0100 Subject: [PATCH 8/8] Only opt out of resetting connection when changing pw --- .../modules/Stream/Auth/ConnectionForm.jsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/browser/modules/Stream/Auth/ConnectionForm.jsx b/src/browser/modules/Stream/Auth/ConnectionForm.jsx index 59733df03d0..df7f381f83c 100644 --- a/src/browser/modules/Stream/Auth/ConnectionForm.jsx +++ b/src/browser/modules/Stream/Auth/ConnectionForm.jsx @@ -67,11 +67,15 @@ export class ConnectionForm extends Component { } ) } - connect = (doneFn = () => {}, onError = null) => { + connect = ( + doneFn = () => {}, + onError = null, + noResetConnectionOnFail = false + ) => { this.props.error({}) this.props.bus.self( CONNECT, - { ...this.state, noResetConnectionOnFail: true }, + { ...this.state, noResetConnectionOnFail }, res => { doneFn() if (res.success) { @@ -141,17 +145,29 @@ export class ConnectionForm extends Component { if (res.error.code === 'Neo.ClientError.Security.Unauthorized') { retries-- if (retries > 0) { - setTimeout(() => this.connect(() => { - this.setState({ isLoading: false }) - }, retryFn), 200) + setTimeout( + () => + this.connect( + () => { + this.setState({ isLoading: false }) + }, + retryFn, + true + ), + 200 + ) } } else { this.props.error(res.error) } } - this.connect(() => { - this.setState({ isLoading: false }) - }, retryFn) + this.connect( + () => { + this.setState({ isLoading: false }) + }, + retryFn, + true + ) }) } this.setState({ isLoading: false })