Port Master Password dialog to React. Fixes #7342
parent
e59471d87d
commit
b283c0ba18
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 |
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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() {
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue