diff --git a/docs/en_US/images/master_password_enter.png b/docs/en_US/images/master_password_enter.png index 7989af751..f27c0c932 100644 Binary files a/docs/en_US/images/master_password_enter.png and b/docs/en_US/images/master_password_enter.png differ diff --git a/docs/en_US/images/master_password_reset.png b/docs/en_US/images/master_password_reset.png index 565613a91..514bd566f 100644 Binary files a/docs/en_US/images/master_password_reset.png and b/docs/en_US/images/master_password_reset.png differ diff --git a/docs/en_US/images/master_password_set.png b/docs/en_US/images/master_password_set.png index 302aa6aa1..62bc94615 100644 Binary files a/docs/en_US/images/master_password_set.png and b/docs/en_US/images/master_password_set.png differ diff --git a/docs/en_US/release_notes_6_12.rst b/docs/en_US/release_notes_6_12.rst index e0a543a9a..d1683d359 100644 --- a/docs/en_US/release_notes_6_12.rst +++ b/docs/en_US/release_notes_6_12.rst @@ -14,6 +14,7 @@ New features Housekeeping ************ + | `Issue #7342 `_ - Port Master Password dialog to React. | `Issue #7492 `_ - Removing dynamic module loading and replacing it with static loading. Bug fixes diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index e02d7cfbe..c8433acb0 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -743,30 +743,11 @@ def get_nodes(): def form_master_password_response(existing=True, present=False, errmsg=None): - content_new = ( - gettext("Set Master Password"), - "
".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"), - "
".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( diff --git a/web/pgadmin/browser/static/js/MasterPassowrdContent.jsx b/web/pgadmin/browser/static/js/MasterPassowrdContent.jsx new file mode 100644 index 000000000..7a3158147 --- /dev/null +++ b/web/pgadmin/browser/static/js/MasterPassowrdContent.jsx @@ -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 ( + + + + + {isPWDPresent ? gettext('Please enter your master password.') : gettext('Please set a master password for pgAdmin.')} + +
+ + {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.')} + +
+ + onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/> + + +
+ + + } onClick={() => { + let _url = url_for('help.static', { + 'filename': 'master_password.html', + }); + window.open(_url, 'pgadmin_help'); + }} > + + {isPWDPresent && + } + onClick={() => {onResetPassowrd?.();}} > + {gettext('Reset Master Password')} + + } + + } onClick={() => { + onCancel?.(); + closeModal(); + }} >{gettext('Cancel')} + } + 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')} + + +
); +} + +MasterPasswordContent.propTypes = { + closeModal: PropTypes.func, + onResetPassowrd: PropTypes.func, + onOK: PropTypes.func, + onCancel: PropTypes.func, + setHeight: PropTypes.func, + isPWDPresent: PropTypes.bool, + data: PropTypes.object, +}; diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 0ca7d11a8..53d58c253 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -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() { diff --git a/web/pgadmin/browser/static/js/password_dialogs.jsx b/web/pgadmin/browser/static/js/password_dialogs.jsx index d5b862667..e00f0e0c9 100644 --- a/web/pgadmin/browser/static/js/password_dialogs.jsx +++ b/web/pgadmin/browser/static/js/password_dialogs.jsx @@ -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() { /> , 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( + + { + 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); + // }); + }} + /> + , j[0]); +} + diff --git a/web/pgadmin/browser/templates/browser/master_password.html b/web/pgadmin/browser/templates/browser/master_password.html deleted file mode 100644 index f6b3d4c44..000000000 --- a/web/pgadmin/browser/templates/browser/master_password.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
-
{{ content_text|safe }}
-
- -
- -
-
- {% if errmsg %} -
- -
- {% endif %} -
-
diff --git a/web/pgadmin/browser/tests/test_master_password.py b/web/pgadmin/browser/tests/test_master_password.py index b53bd9ef2..50ae20c63 100644 --- a/web/pgadmin/browser/tests/test_master_password.py +++ b/web/pgadmin/browser/tests/test_master_password.py @@ -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' diff --git a/web/pgadmin/static/js/helpers/Notifier.jsx b/web/pgadmin/static/js/helpers/Notifier.jsx index c54d00cdd..95d837e8e 100644 --- a/web/pgadmin/static/js/helpers/Notifier.jsx +++ b/web/pgadmin/static/js/helpers/Notifier.jsx @@ -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'); }