Port Master Password dialog to React. Fixes #7342

pull/87/head
Nikhil Mohite 2022-07-04 12:16:23 +05:30 committed by Akshay Joshi
parent e59471d87d
commit b283c0ba18
11 changed files with 238 additions and 197 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -14,6 +14,7 @@ New features
Housekeeping
************
| `Issue #7342 <https://redmine.postgresql.org/issues/7342>`_ - Port Master Password dialog to React.
| `Issue #7492 <https://redmine.postgresql.org/issues/7492>`_ - Removing dynamic module loading and replacing it with static loading.
Bug fixes

View File

@ -743,30 +743,11 @@ def get_nodes():
def form_master_password_response(existing=True, present=False, errmsg=None):
content_new = (
gettext("Set Master Password"),
"<br/>".join([
gettext("Please set a master password for pgAdmin."),
gettext("This will be used to secure and later unlock saved "
"passwords and other credentials.")])
)
content_existing = (
gettext("Unlock Saved Passwords"),
"<br/>".join([
gettext("Please enter your master password."),
gettext("This is required to unlock saved passwords and "
"reconnect to the database server(s).")])
)
return make_json_response(data={
'present': present,
'title': content_existing[0] if existing else content_new[0],
'content': render_template(
'browser/master_password.html',
content_text=content_existing[1] if existing else content_new[1],
errmsg=errmsg
),
'reset': existing
'reset': existing,
'errmsg': errmsg,
'is_error': True if errmsg else False
})
@ -814,11 +795,15 @@ def set_master_password():
data = None
if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8')
if request.form:
data = request.form
elif request.data:
data = request.data
if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8')
if data != '':
data = json.loads(data)
if data != '':
data = json.loads(data)
# Master password is not applicable for server mode
# Enable master password if oauth is used
@ -828,7 +813,7 @@ def set_master_password():
and config.MASTER_PASSWORD_REQUIRED:
# if master pass is set previously
if current_user.masterpass_check is not None and \
data.get('button_click') and \
data.get('submit_password', False) and \
not validate_master_password(data.get('password')):
return form_master_password_response(
existing=True,
@ -864,7 +849,7 @@ def set_master_password():
)
elif not get_crypt_key()[1]:
error_message = None
if data.get('button_click') and data.get('password') == '':
if data.get('submit_password') and data.get('password') == '':
# If user attempted to enter a blank password, then throw error
error_message = gettext("Master password cannot be empty")
return form_master_password_response(

View File

@ -0,0 +1,115 @@
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import { Box } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/CloseRounded';
import DeleteForeverIcon from '@material-ui/icons/DeleteForever';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import HelpIcon from '@material-ui/icons/Help';
import { DefaultButton, PrimaryButton, PgIconButton } from '../../../static/js/components/Buttons';
import { useModalStyles } from '../../../static/js/helpers/ModalProvider';
import { FormFooterMessage, InputText, MESSAGE_TYPE } from '../../../static/js/components/FormComponents';
export default function MasterPasswordContent({ closeModal, onResetPassowrd, onOK, onCancel, setHeight, isPWDPresent, data}) {
const classes = useModalStyles();
const containerRef = useRef();
const firstEleRef = useRef();
const okBtnRef = useRef();
const [formData, setFormData] = useState({
password: ''
});
const onTextChange = (e, id) => {
let val = e;
if (e && e.target) {
val = e.target.value;
}
setFormData((prev) => ({ ...prev, [id]: val }));
};
const onKeyDown = (e) => {
// If enter key is pressed then click on OK button
if (e.key === 'Enter') {
okBtnRef.current?.click();
}
};
useEffect(() => {
setTimeout(() => {
firstEleRef.current && firstEleRef.current.focus();
}, 275);
}, []);
useEffect(() => {
setHeight?.(containerRef.current?.offsetHeight);
}, [containerRef.current]);
return (
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
<Box flexGrow="1" p={2}>
<Box>
<span style={{ fontWeight: 'bold' }}>
{isPWDPresent ? gettext('Please enter your master password.') : gettext('Please set a master password for pgAdmin.')}
</span>
<br />
<span style={{ fontWeight: 'bold' }}>
{isPWDPresent ? gettext('This is required to unlock saved passwords and reconnect to the database server(s).') : gettext('This will be used to secure and later unlock saved passwords and other credentials.')}
</span>
</Box>
<Box marginTop='12px'>
<InputText inputRef={firstEleRef} type="password" value={formData['password']} maxLength={null}
onChange={(e) => onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/>
</Box>
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={data.errmsg} closable={false} style={{
position: 'unset', padding: '12px 0px 0px'
}} />
</Box>
<Box className={classes.footer}>
<Box style={{ marginRight: 'auto' }}>
<PgIconButton data-test="help-masterpassword" title={gettext('Help')} style={{ padding: '0.3rem', paddingLeft: '0.7rem' }} startIcon={<HelpIcon />} onClick={() => {
let _url = url_for('help.static', {
'filename': 'master_password.html',
});
window.open(_url, 'pgadmin_help');
}} >
</PgIconButton>
{isPWDPresent &&
<DefaultButton data-test="reset-masterpassword" style={{ marginLeft: '0.5rem' }} startIcon={<DeleteForeverIcon />}
onClick={() => {onResetPassowrd?.();}} >
{gettext('Reset Master Password')}
</DefaultButton>
}
</Box>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
onCancel?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton ref={okBtnRef} data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />}
disabled={formData.password.length == 0}
onClick={() => {
let postFormData = new FormData();
postFormData.append('password', formData.password);
postFormData.append('submit_password', true);
onOK?.(postFormData);
closeModal();
}}
>
{gettext('OK')}
</PrimaryButton>
</Box>
</Box>);
}
MasterPasswordContent.propTypes = {
closeModal: PropTypes.func,
onResetPassowrd: PropTypes.func,
onOK: PropTypes.func,
onCancel: PropTypes.func,
setHeight: PropTypes.func,
isPWDPresent: PropTypes.bool,
data: PropTypes.object,
};

View File

@ -9,6 +9,7 @@
import { generateNodeUrl } from './node_ajax';
import Notify, {initializeModalProvider, initializeNotifier} from '../../../static/js/helpers/Notifier';
import { checkMasterPassword } from './password_dialogs';
define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore',
@ -569,101 +570,6 @@ define('pgadmin.browser', [
Notify.alert(error);
});
},
init_master_password: function() {
let self = this;
// Master password dialog
if (!Alertify.dlgMasterPass) {
Alertify.dialog('dlgMasterPass', function factory() {
return {
main: function(title, message, reset) {
this.set('title', title);
this.message = message;
this.reset = reset;
},
build: function() {
Alertify.pgDialogBuild.apply(this);
},
setup:function() {
return {
buttons:[{
text: '',
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Master password'),
url: url_for('help.static', {
'filename': 'master_password.html',
}),
},
},{
text: gettext('Reset Master Password'), className: 'btn btn-secondary fa fa-trash-alt pg-alertify-button pull-left',
},{
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
key: 27,
},{
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
}],
focus: {element: '#password', select: true},
options: {
modal: true, resizable: false, maximizable: false, pinnable: false,
},
};
},
prepare:function() {
let _self = this;
_self.setContent(_self.message);
/* Reset button hide */
if(!_self.reset) {
$(_self.__internal.buttons[1].element).addClass('d-none');
} else {
$(_self.__internal.buttons[1].element).removeClass('d-none');
}
},
callback: function(event) {
let parentDialog = this;
if (event.index == 3) {
/* OK Button */
self.set_master_password(
$('#frmMasterPassword #password').val(),
true,parentDialog.set_callback,
);
} else if(event.index == 2) {
/* Cancel button */
self.masterpass_callback_queue = [];
self.cancel_callback();
} else if(event.index == 1) {
/* Reset Button */
event.cancel = true;
Notify.confirm(gettext('Reset Master Password'),
gettext('This will remove all the saved passwords. This will also remove established connections to '
+ 'the server and you may need to reconnect again. Do you wish to continue?'),
function() {
/* If user clicks Yes */
self.reset_master_password();
parentDialog.close();
return true;
},
function() {/* If user clicks No */ return true;}
);
} else if(event.index == 0) {
/* help Button */
event.cancel = true;
self.showHelp(
event.button.element.name,
event.button.element.getAttribute('url'),
null, null
);
return;
}
},
};
});
}
},
check_master_password: function(on_resp_callback) {
$.ajax({
url: url_for('browser.check_master_password'),
@ -697,40 +603,21 @@ define('pgadmin.browser', [
});
},
set_master_password: function(password='', button_click=false,
set_master_password: function(password='',
set_callback=()=>{/*This is intentional (SonarQube)*/},
cancel_callback=()=>{/*This is intentional (SonarQube)*/}) {
let data=null, self = this;
data = JSON.stringify({
// data = JSON.stringify({
// 'password': password,
// });
data = {
'password': password,
'button_click': button_click,
});
};
self.masterpass_callback_queue.push(set_callback);
self.cancel_callback = cancel_callback;
$.ajax({
url: url_for('browser.set_master_password'),
type: 'POST',
data: data,
dataType: 'json',
contentType: 'application/json',
}).done((res)=> {
if(!res.data.present) {
self.init_master_password();
Alertify.dlgMasterPass(res.data.title, res.data.content, res.data.reset);
} else {
setTimeout(()=>{
while(self.masterpass_callback_queue.length > 0) {
let callback = self.masterpass_callback_queue.shift();
callback();
}
}, 500);
}
}).fail(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);
});
// Check master passowrd.
checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
},
bind_beforeunload: function() {

View File

@ -13,7 +13,11 @@ import pgAdmin from 'sources/pgadmin';
import ConnectServerContent from './ConnectServerContent';
import Theme from 'sources/Theme';
import url_for from 'sources/url_for';
import gettext from 'sources/gettext';
import getApiInstance from '../../../static/js/api_instance';
import MasterPasswordContent from './MasterPassowrdContent';
import Notify from '../../../static/js/helpers/Notifier';
function setNewSize(panel, width, height) {
// Add height of the header
@ -130,3 +134,92 @@ export function showSchemaDiffServerPassword() {
/>
</Theme>, j[0]);
}
function masterPassCallbacks(masterpass_callback_queue) {
while(masterpass_callback_queue.length > 0) {
let callback = masterpass_callback_queue.shift();
callback();
}
}
export function checkMasterPassword(data, masterpass_callback_queue, cancel_callback) {
const api = getApiInstance();
api.post(url_for('browser.set_master_password'), data).then((res)=> {
if(!res.data.data.present) {
showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback);
} else {
masterPassCallbacks(masterpass_callback_queue);
}
}).catch(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);
});
}
// This functions is used to show the master password dialog.
export function showMasterPassword(isPWDPresent, errmsg=null, masterpass_callback_queue, cancel_callback) {
const api = getApiInstance();
var pgBrowser = pgAdmin.Browser;
// Register dialog panel
pgBrowser.Node.registerUtilityPanel();
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
j = panel.$container.find('.obj_properties').first();
let title = isPWDPresent ? gettext('Unlock Saved Passwords') : gettext('Set Master Password');
panel.title(title);
ReactDOM.render(
<Theme>
<MasterPasswordContent
isPWDPresent= {isPWDPresent}
data={{'errmsg': errmsg}}
setHeight={(containerHeight) => {
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
}}
closeModal={() => {
panel.close();
}}
onResetPassowrd={()=>{
Notify.confirm(gettext('Reset Master Password'),
gettext('This will remove all the saved passwords. This will also remove established connections to '
+ 'the server and you may need to reconnect again. Do you wish to continue?'),
function() {
var _url = url_for('browser.reset_master_password');
api.delete(_url)
.then(() => {
panel.close();
showMasterPassword(false, null, masterpass_callback_queue, cancel_callback);
})
.catch((err) => {
Notify.error(err.message);
});
return true;
},
function() {/* If user clicks No */ return true;}
);
}}
onCancel={()=>{
cancel_callback?.();
}}
onOK={(formData) => {
panel.close();
checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
// var _url = url_for('browser.set_master_password');
// api.post(_url, formData)
// .then(res => {
// panel.close();
// if(res.data.data.is_error) {
// showMasterPassword(true, res.data.data.errmsg, masterpass_callback_queue, cancel_callback);
// } else {
// masterPassCallbacks(masterpass_callback_queue);
// }
// })
// .catch((err) => {
// Notify.error(err.message);
// });
}}
/>
</Theme>, j[0]);
}

View File

@ -1,23 +0,0 @@
<form name="frmMasterPassword" id="frmMasterPassword" style="height: 100%; width: 100%" onsubmit="return false;">
<div>
<div><strong>{{ content_text|safe }}</strong></div>
<div class="input-group row py-2">
<label for="password" class="col-sm-2 col-form-label">{{ _('Password') }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password" name="password">
</div>
</div>
{% if errmsg %}
<div class='pg-prop-status-bar p-0'>
<div class="error-in-footer">
<div class="d-flex px-2 py-1">
<div class="pr-2">
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
</div>
<div class="alert-text">{{ errmsg }}</div>
</div>
</div>
</div>
{% endif %}
</div>
</form>

View File

@ -25,30 +25,20 @@ class MasterPasswordTestCase(BaseTestGenerator):
# This testcase validates invalid confirmation password
('TestCase for Create master password dialog', dict(
password="",
content=(
"Set Master Password",
[
"Please set a master password for pgAdmin.",
"This will be used to secure and later unlock saved "
"passwords and other credentials."
]
)
errmsg=None,
is_error=False
)),
('TestCase for Setting Master Password', dict(
password="masterpasstest",
check_if_set=True,
errmsg=None,
is_error=False
)),
('TestCase for Resetting Master Password', dict(
reset=True,
password="",
content=(
"Set Master Password",
[
"Please set a master password for pgAdmin.",
"This will be used to secure and later unlock saved "
"passwords and other credentials."
]
)
errmsg=None,
is_error=False
)),
]
@ -90,13 +80,6 @@ class MasterPasswordTestCase(BaseTestGenerator):
)
self.assertEqual(response.status_code, 200)
if hasattr(self, 'content'):
self.assertEqual(response.json['data']['title'],
self.content[0])
for text in self.content[1]:
self.assertIn(text, response.json['data']['content'])
if hasattr(self, 'check_if_set'):
response = self.tester.get(
'/browser/master_password'

View File

@ -196,7 +196,7 @@ var Notifier = {
if(resp.info == 'CRYPTKEY_MISSING') {
var pgBrowser = window.pgAdmin.Browser;
pgBrowser.set_master_password('', false, ()=> {
pgBrowser.set_master_password('', ()=> {
if(onJSONResult && typeof(onJSONResult) == 'function') {
onJSONResult('CRYPTKEY_SET');
}